From 14a1eb041499fa27b0801593dc481906c784cdff Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 12 Dec 2013 17:47:49 -0800 Subject: [PATCH] Adding Orchard.Messaging and Orchard.Templates --- ClickToBuildAzurePackage.cmd | 32 + .../Stubs/StubHttpContextAccessor.cs | 4 + .../Orchard.Email/Activities/EmailActivity.cs | 63 + .../Orchard.Email/Activities/MailActivity.cs | 9 +- .../Modules/Orchard.Email/Forms/EmailForm.cs | 110 + .../Orchard.Email/Models/EmailMessage.cs | 15 + .../Modules/Orchard.Email/Module.txt | 2 +- .../Orchard.Email/Orchard.Email.csproj | 14 +- .../Services/EmailMessageChannel.cs | 77 + .../Services/EmailMessagingChannel.cs | 1 + ...workflows-activity-sendemaildeprecated.css | 17 + .../Views/Activity-SendEmail.cshtml | 7 +- .../Views/Activity-SendEmailDeprecated.cshtml | 7 + .../Modules/Orchard.Messaging/AdminMenu.cs | 34 + .../Controllers/AdminMessageController.cs | 31 + .../Controllers/AdminPriorityController.cs | 86 + .../Controllers/AdminQueueController.cs | 138 + .../Extensions/StringExtensions.cs | 7 + .../Modules/Orchard.Messaging/Migrations.cs | 11 - .../Migrations/MessagingMigrations.cs | 14 + .../Migrations/MessagingQueuingMigrations.cs | 50 + .../Models/MessagePriority.cs | 16 + .../Orchard.Messaging/Models/MessageQueue.cs | 39 + .../Models/MessageQueueRecord.cs | 15 + .../Models/MessageQueueStatus.cs | 7 + .../Models/MessageRecipient.cs | 15 + .../Orchard.Messaging/Models/QueuedMessage.cs | 80 + .../Models/QueuedMessageRecord.cs | 25 + .../Models/QueuedMessageStatus.cs | 8 + .../Modules/Orchard.Messaging/Module.txt | 13 +- .../Orchard.Messaging.csproj | 98 +- .../Orchard.Messaging/Scripts/Web.config | 25 + .../Orchard.Messaging/Scripts/submit.js | 8 + .../Orchard.Messaging/Scripts/submit.min.js | 2 + .../Scripts/submit.min.js.map | 8 + .../Services/IMessageChannel.cs | 9 + .../Services/MessageChannelBase.cs | 24 + .../Services/MessageQueueBackgroundTask.cs | 16 + .../Services/MessageQueueManager.cs | 284 + .../Services/MessageQueueProcessor.cs | 76 + .../Styles/Images/menu.messaging.png | Bin 0 -> 1184 bytes .../Styles/Images/message-status.png | Bin 0 -> 3047 bytes .../Styles/Images/queue-controls.png | Bin 0 -> 1183 bytes .../Orchard.Messaging/Styles/Web.config | 25 + .../Styles/admin-messaging.css | 69 + .../Styles/admin-messaging.min.css | 1 + .../Styles/menu.messaging-admin.css | 7 + .../Styles/workflows-activity-sendmessage.css | 17 + .../Tests/MessageQueueProcessorTests.cs | 68 + .../Tests/MessageQueueTests.cs | 62 + .../Tests/Orchard.Messaging.Tests.csproj | 97 + .../Tests/Properties/AssemblyInfo.cs | 35 + .../Tests/StubMessageChannel.cs | 26 + .../ViewModels/MessagePriorityViewModel.cs | 14 + .../ViewModels/MessageQueueViewModel.cs | 12 + .../ViewModels/MessagesFilter.cs | 7 + .../Views/Activity-SendMessage.cshtml | 4 + .../Views/AdminMessage/Details.cshtml | 47 + .../Views/AdminPriority/Create.cshtml | 10 + .../Views/AdminPriority/Edit.cshtml | 9 + .../Views/AdminPriority/Index.cshtml | 40 + .../Views/AdminQueue/Create.cshtml | 9 + .../Views/AdminQueue/Edit.cshtml | 10 + .../Views/AdminQueue/Index.cshtml | 62 + .../Views/AdminQueue/List.cshtml | 74 + .../MessagePriorityViewModel.cshtml | 17 + .../MessageQueueViewModel.cshtml | 18 + .../Orchard.SecureSocketsLayer.csproj | 12 + .../web.Debug.config | 30 + .../web.Release.config | 31 + .../Modules/Orchard.Templates/AdminMenu.cs | 13 + .../Controllers/AdminController.cs | 174 + .../Drivers/ShapePartDriver.cs | 75 + .../Handlers/ShapePartHandler.cs | 16 + .../Helpers/StringExtensions.cs | 7 + .../Modules/Orchard.Templates/Migrations.cs | 26 + .../Orchard.Templates/Models/ShapePart.cs | 30 + .../Modules/Orchard.Templates/Module.txt | 13 + .../Orchard.Templates.csproj | 240 + .../Modules/Orchard.Templates/Placement.info | 3 + .../Properties/AssemblyInfo.cs | 36 + .../Orchard.Templates/Scripts/Web.config | 25 + .../Scripts/codemirror/keymap/emacs.js | 387 ++ .../Scripts/codemirror/keymap/extra.js | 43 + .../Scripts/codemirror/keymap/vim.js | 3704 +++++++++++ .../Scripts/codemirror/lib/codemirror.css | 263 + .../Scripts/codemirror/lib/codemirror.js | 5910 +++++++++++++++++ .../Scripts/codemirror/mode/css/css.js | 639 ++ .../Scripts/codemirror/mode/css/index.html | 70 + .../Scripts/codemirror/mode/css/scss.html | 157 + .../Scripts/codemirror/mode/css/scss_test.js | 84 + .../Scripts/codemirror/mode/css/test.js | 130 + .../mode/htmlembedded/htmlembedded.js | 73 + .../codemirror/mode/htmlembedded/index.html | 60 + .../codemirror/mode/htmlmixed/htmlmixed.js | 104 + .../codemirror/mode/htmlmixed/index.html | 85 + .../codemirror/mode/javascript/index.html | 107 + .../codemirror/mode/javascript/javascript.js | 480 ++ .../codemirror/mode/javascript/test.js | 10 + .../mode/javascript/typescript.html | 61 + .../codemirror/mode/markdown/index.html | 359 + .../codemirror/mode/markdown/markdown.js | 551 ++ .../Scripts/codemirror/mode/markdown/test.js | 656 ++ .../Scripts/codemirror/mode/meta.js | 89 + .../Scripts/codemirror/mode/xml/index.html | 57 + .../Scripts/codemirror/mode/xml/xml.js | 345 + .../Scripts/codemirror/theme/3024-day.css | 34 + .../Scripts/codemirror/theme/3024-night.css | 34 + .../codemirror/theme/ambiance-mobile.css | 5 + .../Scripts/codemirror/theme/ambiance.css | 75 + .../Scripts/codemirror/theme/base16-dark.css | 34 + .../Scripts/codemirror/theme/base16-light.css | 34 + .../Scripts/codemirror/theme/blackboard.css | 28 + .../Scripts/codemirror/theme/cobalt.css | 21 + .../Scripts/codemirror/theme/eclipse.css | 23 + .../Scripts/codemirror/theme/elegant.css | 13 + .../Scripts/codemirror/theme/erlang-dark.css | 30 + .../Scripts/codemirror/theme/lesser-dark.css | 47 + .../Scripts/codemirror/theme/mbo.css | 35 + .../Scripts/codemirror/theme/midnight.css | 43 + .../Scripts/codemirror/theme/monokai.css | 29 + .../Scripts/codemirror/theme/neat.css | 12 + .../Scripts/codemirror/theme/night.css | 24 + .../Scripts/codemirror/theme/paraiso-dark.css | 34 + .../codemirror/theme/paraiso-light.css | 34 + .../Scripts/codemirror/theme/rubyblue.css | 23 + .../Scripts/codemirror/theme/solarized.css | 180 + .../Scripts/codemirror/theme/the-matrix.css | 26 + .../theme/tomorrow-night-eighties.css | 34 + .../Scripts/codemirror/theme/twilight.css | 28 + .../Scripts/codemirror/theme/vibrant-ink.css | 30 + .../Scripts/codemirror/theme/xq-dark.css | 49 + .../Scripts/codemirror/theme/xq-light.css | 43 + .../Scripts/template-editor.js | 19 + .../Services/DefaultTemplateService.cs | 98 + .../Services/ITemplateProcessor.cs | 9 + .../Services/ITemplateService.cs | 16 + .../Services/RazorTemplateProcessor.cs | 77 + .../Services/StubHttpContext.cs | 101 + .../Services/TemplateBindingStrategy.cs | 136 + .../Services/TemplateProcessorImpl.cs | 10 + .../Orchard.Templates/Styles/Web.config | 25 + .../Styles/menu.templates-admin.css | 7 + .../Styles/menu.templates.png | Bin 0 -> 1254 bytes .../Styles/template-editor.css | 4 + .../Orchard.Templates.Tests.csproj | 86 + .../Properties/AssemblyInfo.cs | 36 + .../RazorParserTests.cs | 24 + .../TemplateServiceTests.cs | 65 + .../Tokens/ShapeTokenProvider.cs | 29 + .../ViewModels/ShapePartViewModel.cs | 13 + .../Views/Admin/CreatableTypeList.cshtml | 5 + .../Orchard.Templates/Views/Admin/List.cshtml | 48 + .../Views/EditorTemplates/Parts.Shape.cshtml | 31 + .../TemplateLanguagePicker.cshtml | 6 + .../Views/SampleShape.cshtml | 1 + .../Orchard.Templates/Views/Web.config | 41 + .../Modules/Orchard.Templates/Web.config | 46 + src/Orchard.Web/Orchard.Web.csproj | 1 - src/Orchard.sln | 39 + src/Orchard/Compilation/CompilersModule.cs | 8 + src/Orchard/Compilation/ICompiler.cs | 8 + .../Compilation/Razor/IRazorCompiler.cs | 9 + .../Compilation/Razor/IRazorTemplateCache.cs | 6 + .../Compilation/Razor/RazorCompiler.cs | 152 + src/Orchard/Compilation/Razor/RazorModule.cs | 10 + .../Compilation/Razor/RazorTemplateBase.cs | 39 + .../Compilation/Razor/RazorTemplateCache.cs | 19 + .../ShapeTemplateBindingStrategy.cs | 51 +- .../Messaging/Services/IMessagingChannel.cs | 4 +- src/Orchard/Mvc/IHttpContextAccessor.cs | 11 +- src/Orchard/Orchard.Framework.csproj | 8 + 172 files changed, 19655 insertions(+), 37 deletions(-) create mode 100644 ClickToBuildAzurePackage.cmd create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Activities/EmailActivity.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Forms/EmailForm.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Models/EmailMessage.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageChannel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Styles/workflows-activity-sendemaildeprecated.css create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmailDeprecated.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/AdminMenu.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminMessageController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminPriorityController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminQueueController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Extensions/StringExtensions.cs delete mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Migrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingMigrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingQueuingMigrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/MessagePriority.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueue.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueStatus.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageRecipient.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessage.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageStatus.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Scripts/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.js create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js.map create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Services/IMessageChannel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageChannelBase.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueBackgroundTask.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueManager.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueProcessor.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/menu.messaging.png create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/message-status.png create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/queue-controls.png create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.css create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.min.css create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/menu.messaging-admin.css create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Styles/workflows-activity-sendmessage.css create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueProcessorTests.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueTests.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Tests/Orchard.Messaging.Tests.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Tests/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Tests/StubMessageChannel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagePriorityViewModel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessageQueueViewModel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagesFilter.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/Activity-SendMessage.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminMessage/Details.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Create.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Edit.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Index.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Create.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Edit.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Index.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/List.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessagePriorityViewModel.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessageQueueViewModel.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Debug.config create mode 100644 src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Release.config create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/AdminMenu.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Controllers/AdminController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Drivers/ShapePartDriver.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Handlers/ShapePartHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Helpers/StringExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Migrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Models/ShapePart.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Module.txt create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Orchard.Templates.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Placement.info create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/emacs.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/extra.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/vim.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/css.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss_test.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/test.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/htmlembedded.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/htmlmixed.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/javascript.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/test.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/typescript.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/markdown.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/test.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/meta.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/index.html create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/xml.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-day.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-night.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance-mobile.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-dark.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-light.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/blackboard.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/cobalt.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/eclipse.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/elegant.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/erlang-dark.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/lesser-dark.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/mbo.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/midnight.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/monokai.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/neat.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/night.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-dark.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-light.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/rubyblue.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/solarized.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/the-matrix.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/tomorrow-night-eighties.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/twilight.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/vibrant-ink.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-dark.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-light.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Scripts/template-editor.js create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/DefaultTemplateService.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateProcessor.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateService.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/StubHttpContext.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateBindingStrategy.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateProcessorImpl.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Styles/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates-admin.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates.png create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Styles/template-editor.css create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Orchard.Templates.Tests.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/RazorParserTests.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/TemplateServiceTests.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Tokens/ShapeTokenProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/ViewModels/ShapePartViewModel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/CreatableTypeList.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/List.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/Parts.Shape.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/TemplateLanguagePicker.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/SampleShape.cshtml create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Views/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Templates/Web.config create mode 100644 src/Orchard/Compilation/CompilersModule.cs create mode 100644 src/Orchard/Compilation/ICompiler.cs create mode 100644 src/Orchard/Compilation/Razor/IRazorCompiler.cs create mode 100644 src/Orchard/Compilation/Razor/IRazorTemplateCache.cs create mode 100644 src/Orchard/Compilation/Razor/RazorCompiler.cs create mode 100644 src/Orchard/Compilation/Razor/RazorModule.cs create mode 100644 src/Orchard/Compilation/Razor/RazorTemplateBase.cs create mode 100644 src/Orchard/Compilation/Razor/RazorTemplateCache.cs diff --git a/ClickToBuildAzurePackage.cmd b/ClickToBuildAzurePackage.cmd new file mode 100644 index 000000000..004332e44 --- /dev/null +++ b/ClickToBuildAzurePackage.cmd @@ -0,0 +1,32 @@ +if "%WindowsSdkDir%" neq "" goto build +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" goto initialize2k8on64Dev11 +if exist "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" goto initialize2k8Dev11 +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" goto initialize2k8on64Dev12 +if exist "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" goto initialize2k8Dev12 +echo "Unable to detect suitable environment. Build may not succeed." +goto build + + +:initialize2k8Dev12 +call "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" x86 +goto build + +:initialize2k8on64Dev12 +call "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" x86 +goto build + +:initialize2k8Dev11 +call "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" x86 +goto build + +:initialize2k8on64Dev11 +call "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" x86 +goto build + +:build +msbuild /t:Build AzurePackage.proj +pause +goto end + + +:end diff --git a/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs b/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs index 45a736932..347599686 100644 --- a/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs +++ b/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs @@ -12,5 +12,9 @@ namespace Orchard.Tests.Stubs { public HttpContextBase Current() { return _httpContext; } + + public void Set(HttpContextBase stub) { + _httpContext = stub; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Activities/EmailActivity.cs b/src/Orchard.Web/Modules/Orchard.Email/Activities/EmailActivity.cs new file mode 100644 index 000000000..3b91dc9cc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Activities/EmailActivity.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Email.Models; +using Orchard.Email.Services; +using Orchard.Localization; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Workflows.Models; +using Orchard.Workflows.Services; + +namespace Orchard.Email.Activities { + public class EmailActivity : Task { + private readonly IMessageQueueManager _messageQueueManager; + + public EmailActivity(IMessageQueueManager messageQueueManager) { + _messageQueueManager = messageQueueManager; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public override IEnumerable GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext) { + return new[] { T("Queued") }; + } + + public override string Form { + get { + return "EmailActivity"; + } + } + + public override LocalizedString Category { + get { return T("Messaging"); } + } + + public override string Name { + get { return "SendEmail"; } + } + + public override LocalizedString Description { + get { return T("Sends an email to a specific user."); } + } + + public override IEnumerable Execute(WorkflowContext workflowContext, ActivityContext activityContext) { + var recipientAddresses = Split(activityContext.GetState("RecipientAddress")).ToList(); + var body = activityContext.GetState("Body"); + var subject = activityContext.GetState("Subject"); + var queueId = activityContext.GetState("Queue") ?? _messageQueueManager.GetDefaultQueue().Id; + var priorityId = activityContext.GetState("Priority"); + var recipients = recipientAddresses.Select(x => new MessageRecipient(x)); + var priority = _messageQueueManager.GetPriority(priorityId); + var payload = new EmailMessage(subject, body); + _messageQueueManager.Send(recipients, EmailMessageChannel.ChannelName, payload, priority, queueId); + + yield return T("Queued"); + } + + private static IEnumerable Split(string value) { + return !String.IsNullOrWhiteSpace(value) ? value.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) : Enumerable.Empty(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Activities/MailActivity.cs b/src/Orchard.Web/Modules/Orchard.Email/Activities/MailActivity.cs index e5a4c3d8b..936d6d242 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Activities/MailActivity.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Activities/MailActivity.cs @@ -12,12 +12,12 @@ using Orchard.Workflows.Models; using Orchard.Workflows.Services; namespace Orchard.Email.Activities { - + [Obsolete("Use the new EmailActivity instead.")] public class MailActivity : Task { private readonly IMessageManager _messageManager; private readonly IOrchardServices _orchardServices; private readonly IMembershipService _membershipService; - public const string MessageType = "ActionEmail"; + public const string MessageType = "ActionEmail_Deprecated"; public MailActivity( IMessageManager messageManager, @@ -46,12 +46,12 @@ namespace Orchard.Email.Activities { } public override string Name { - get { return "SendEmail"; } + get { return "SendEmailDeprecated"; } } public override LocalizedString Description { - get { return T("Sends an e-mail to a specific user."); } + get { return T("Sends an e-mail to a specific user (deprecated)."); } } public override IEnumerable Execute(WorkflowContext workflowContext, ActivityContext activityContext) { @@ -103,6 +103,7 @@ namespace Orchard.Email.Activities { } } + [Obsolete] public class MailActionsHandler : IMessageEventHandler { public MailActionsHandler() { T = NullLocalizer.Instance; diff --git a/src/Orchard.Web/Modules/Orchard.Email/Forms/EmailForm.cs b/src/Orchard.Web/Modules/Orchard.Email/Forms/EmailForm.cs new file mode 100644 index 000000000..3fd3e612b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Forms/EmailForm.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web.Mvc; +using Orchard.DisplayManagement; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; + +namespace Orchard.Email.Forms { + public class EmailForm : Component, IFormProvider { + private readonly IMessageQueueManager _messageQueueManager; + protected dynamic New { get; set; } + + public EmailForm(IShapeFactory shapeFactory, IMessageQueueManager messageQueueManager) { + New = shapeFactory; + _messageQueueManager = messageQueueManager; + } + + public void Describe(DescribeContext context) { + Func formFactory = + shape => { + var form = New.Form( + Id: "EmailActivity", + _Type: New.FieldSet( + Title: T("Send to"), + _RecipientAddress: New.Textbox( + Id: "recipient-address", + Name: "RecipientAddress", + Title: T("Email Address"), + Description: T("Specify a comma-separated list of recipient email addresses. To include a display name, use the following format: John Doe <john.doe@outlook.com>"), + Classes: new[] {"large", "text", "tokenized"}), + _Subject: New.Textbox( + Id: "Subject", Name: "Subject", + Title: T("Subject"), + Description: T("The subject of the email message."), + Classes: new[] {"large", "text", "tokenized"}), + _Message: New.Textarea( + Id: "Body", Name: "Body", + Title: T("Body"), + Description: T("The body of the email message."), + Classes: new[] {"tokenized"}), + _Priority: New.SelectList( + Id: "priority", + Name: "Priority", + Title: T("Priority"), + Description: ("The priority of this message."), + Items: GetPriorities()))); + + var queues = GetQueues(); + + if (queues.Count() > 1) { + form._Queue = New.SelectList( + Id: "queue", + Name: "Queue", + Title: T("Queue"), + Description: ("The queue to add this message to."), + Items: queues); + } + + return form; + }; + + context.Form("EmailActivity", formFactory); + } + + private IEnumerable GetPriorities() { + var priorities = _messageQueueManager.GetPriorities().ToList(); + if (!priorities.Any()) + priorities = _messageQueueManager.CreateDefaultPriorities().ToList(); + return priorities.Select(x => new SelectListItem { Text = x.DisplayText, Value = x.Id.ToString(CultureInfo.InvariantCulture) }).ToList(); + } + + private IEnumerable GetQueues() { + var queues = _messageQueueManager.GetQueues().ToList(); + if (!queues.Any()) + queues = new List {_messageQueueManager.CreateDefaultQueue()}; + return queues.Select(x => new SelectListItem {Text = x.Name, Value = x.Id.ToString(CultureInfo.InvariantCulture)}).ToList(); + } + } + + public class EmailFormValidator : IFormEventHandler { + public Localizer T { get; set; } + public void Building(BuildingContext context) {} + public void Built(BuildingContext context) {} + public void Validated(ValidatingContext context) { } + + public void Validating(ValidatingContext context) { + if (context.FormName != "EmailActivity") return; + + var recipientAddress = context.ValueProvider.GetValue("RecipientAddress").AttemptedValue; + var subject = context.ValueProvider.GetValue("Subject").AttemptedValue; + var body = context.ValueProvider.GetValue("Body").AttemptedValue; + + if (String.IsNullOrWhiteSpace(recipientAddress)) { + context.ModelState.AddModelError("RecipientAddress", T("You must specify at least one recipient.").Text); + } + + if (String.IsNullOrWhiteSpace(subject)) { + context.ModelState.AddModelError("Subject", T("You must provide a Subject.").Text); + } + + if (String.IsNullOrWhiteSpace(body)) { + context.ModelState.AddModelError("Body", T("You must provide a Body.").Text); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/EmailMessage.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/EmailMessage.cs new file mode 100644 index 000000000..9126f6387 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/EmailMessage.cs @@ -0,0 +1,15 @@ +namespace Orchard.Email.Models { + public class EmailMessage { + public EmailMessage() { + + } + + public EmailMessage(string subject, string body) { + Subject = subject; + Body = body; + } + + public string Subject { get; set; } + public string Body { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Module.txt b/src/Orchard.Web/Modules/Orchard.Email/Module.txt index ac169294e..5518cf489 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Email/Module.txt @@ -10,4 +10,4 @@ Features: Name: Email Messaging FeatureDescription: Email Messaging services. Category: Messaging - Dependencies: Orchard.Messaging, Orchard.Workflows + Dependencies: Orchard.Messaging.Queuing, Orchard.Workflows diff --git a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj index 64c29b457..c9eec8657 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj +++ b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj @@ -64,14 +64,18 @@ + + + + @@ -79,6 +83,7 @@ + @@ -96,6 +101,10 @@ {642a49d7-8752-4177-80d6-bfbbcfad3de0} Orchard.Forms + + {085948ff-0e9b-4a9a-b564-f8b8b4bdddbc} + Orchard.Messaging + {7059493c-8251-4764-9c1e-2368b8b485bc} Orchard.Workflows @@ -108,11 +117,14 @@ - + + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageChannel.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageChannel.cs new file mode 100644 index 000000000..6578e3cb4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageChannel.cs @@ -0,0 +1,77 @@ +using System; +using System.Net; +using System.Net.Mail; +using Orchard.ContentManagement; +using Orchard.Logging; +using Orchard.Email.Models; +using Orchard.Messaging.Services; +using Orchard.Messaging.Models; + +namespace Orchard.Email.Services { + public interface IEmailMessageChannel : IMessageChannel { } + + public class EmailMessageChannel : MessageChannelBase, IEmailMessageChannel { + private readonly IOrchardServices _services; + private readonly Lazy _smtpClientField; + public const string ChannelName = "Email"; + + public EmailMessageChannel(IOrchardServices services) { + _services = services; + _smtpClientField = new Lazy(CreateSmtpClient); + Logger = NullLogger.Instance; + } + + public override string Name { + get { return ChannelName; } + } + + private SmtpClient SmtpClient { + get { return _smtpClientField.Value; } + } + + protected override void Dispose(bool disposing) { + if (!disposing) return; + if (!_smtpClientField.IsValueCreated) return; + + _smtpClientField.Value.Dispose(); + } + + public override void Send(QueuedMessage message) { + var smtpSettings = _services.WorkContext.CurrentSite.As(); + if (smtpSettings == null || !smtpSettings.IsValid()) return; + + var emailPayload = message.GetPayload(); + var mailMessage = new MailMessage { + From = new MailAddress(smtpSettings.Address), + Subject = emailPayload.Subject, + Body = emailPayload.Body, + IsBodyHtml = emailPayload.Body != null && emailPayload.Body.Contains("<") && emailPayload.Body.Contains(">") + }; + + foreach (var recipient in message.Recipients) { + mailMessage.To.Add(new MailAddress(recipient.AddressOrAlias)); + } + + SmtpClient.Send(mailMessage); + } + + private SmtpClient CreateSmtpClient() { + var smtpSettings = _services.WorkContext.CurrentSite.As(); + var smtpClient = new SmtpClient { + UseDefaultCredentials = !smtpSettings.RequireCredentials + }; + + if (!smtpClient.UseDefaultCredentials && !String.IsNullOrWhiteSpace(smtpSettings.UserName)) { + smtpClient.Credentials = new NetworkCredential(smtpSettings.UserName, smtpSettings.Password); + } + + if (smtpSettings.Host != null) + smtpClient.Host = smtpSettings.Host; + + smtpClient.Port = smtpSettings.Port; + smtpClient.EnableSsl = smtpSettings.EnableSsl; + smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; + return smtpClient; + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs index e2781649a..aed9093bc 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs @@ -10,6 +10,7 @@ using Orchard.Messaging.Services; using Orchard.Messaging.Models; namespace Orchard.Email.Services { + [Obsolete("Use EmailMessageChannel instead.")] public class EmailMessagingChannel : IMessagingChannel { private readonly IOrchardServices _orchardServices; diff --git a/src/Orchard.Web/Modules/Orchard.Email/Styles/workflows-activity-sendemaildeprecated.css b/src/Orchard.Web/Modules/Orchard.Email/Styles/workflows-activity-sendemaildeprecated.css new file mode 100644 index 000000000..b10cd302c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Styles/workflows-activity-sendemaildeprecated.css @@ -0,0 +1,17 @@ +.envelope { + width: 36px; + height: 36px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAARhJREFUSEvtlkEORTAQhnsnSxsre+dwBCdwBidwAyewl0jsbJ3Ahsi8jvfK6JsnqGLxFp/IaGf+jr9NBQDcChu8kumlbVsIggAcx7GK53mAtRYChmEQURSxE2wQhiF0XTcL6PteXLF6xacLD+oAkmUZO9gGaZrKkpoJkSRJ2AlnEsexLDXXXAhAbIrQiyPjA01YFIV8fQfLsmQTmJDn+ZS/qqqlCdUuoArPMiY1HIId/toFdBu6rgt1XcuweTfoqpumAd/3x/iqAAV16t5zQj/t9B22SQAZ+DMRxxbhmwUoaFLaSor+69bE7haAcGZS3/aa95AABTWWzlbDGglAaALT+YcSmPIX8DwBj7iQyC7cdym9EzZ4HSBeOGnowJmFUWAAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: center; +} + +.toolbox-task.toolbox-send-email-deprecated { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAARhJREFUSEvtlkEORTAQhnsnSxsre+dwBCdwBidwAyewl0jsbJ3Ahsi8jvfK6JsnqGLxFp/IaGf+jr9NBQDcChu8kumlbVsIggAcx7GK53mAtRYChmEQURSxE2wQhiF0XTcL6PteXLF6xacLD+oAkmUZO9gGaZrKkpoJkSRJ2AlnEsexLDXXXAhAbIrQiyPjA01YFIV8fQfLsmQTmJDn+ZS/qqqlCdUuoArPMiY1HIId/toFdBu6rgt1XcuweTfoqpumAd/3x/iqAAV16t5zQj/t9B22SQAZ+DMRxxbhmwUoaFLaSor+69bE7haAcGZS3/aa95AABTWWzlbDGglAaALT+YcSmPIX8DwBj7iQyC7cdym9EzZ4HSBeOGnowJmFUWAAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: 5px 5px; +} + +.toolbox-task.toolbox-send-email-deprecated div { + margin-left: 36px; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmail.cshtml b/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmail.cshtml index 24b2670a5..c19c22d39 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmail.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmail.cshtml @@ -1,7 +1,4 @@ -@using Orchard.Utility.Extensions - -
-
- @*@name.CamelFriendly()*@ +
+
diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmailDeprecated.cshtml b/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmailDeprecated.cshtml new file mode 100644 index 000000000..24b2670a5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/Activity-SendEmailDeprecated.cshtml @@ -0,0 +1,7 @@ +@using Orchard.Utility.Extensions + +
+
+ @*@name.CamelFriendly()*@ +
+ diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.Messaging/AdminMenu.cs new file mode 100644 index 000000000..015a52b65 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/AdminMenu.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Orchard.Environment.Extensions; +using Orchard.Messaging.Services; +using Orchard.UI.Navigation; + +namespace Orchard.Messaging { + [OrchardFeature("Orchard.Messaging.Queuing")] + public class AdminMenu : Component, INavigationProvider { + private readonly IMessageQueueManager _messageQueueManager; + + public AdminMenu(IMessageQueueManager messageQueueManager) { + _messageQueueManager = messageQueueManager; + } + + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) { + var queues = _messageQueueManager.GetQueues().ToList(); + builder + .AddImageSet("messaging") + .Add(T("Messaging"), "5.0", item => { + if (queues.Count == 1) { + item.Action("List", "AdminQueue", new { area = "Orchard.Messaging", id = queues.First().Id }); + item.LinkToFirstChild(false); + } + else { + item.Action("Index", "AdminQueue", new { area = "Orchard.Messaging" }); + } + item.Add(T("Priorities"), "1.1", subItem => subItem + .Action("Index", "AdminPriority", new { area = "Orchard.Messaging" })); + }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminMessageController.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminMessageController.cs new file mode 100644 index 000000000..f8d4176ec --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminMessageController.cs @@ -0,0 +1,31 @@ +using System.Web.Mvc; +using Orchard.DisplayManagement; +using Orchard.Environment.Extensions; +using Orchard.Messaging.Services; +using Orchard.UI.Admin; + +namespace Orchard.Messaging.Controllers { + [OrchardFeature("Orchard.Messaging.Queuing")] + [Admin] + public class AdminMessageController : Controller { + private readonly IMessageQueueManager _messageQueueManager; + + public AdminMessageController(IMessageQueueManager messageQueueManager, IShapeFactory shapeFactory) { + _messageQueueManager = messageQueueManager; + New = shapeFactory; + } + + public dynamic New { get; set; } + + public ActionResult Details(int id, string returnUrl) { + var message = _messageQueueManager.GetMessage(id); + + if (!Url.IsLocalUrl(returnUrl)) + returnUrl = Url.Action("List", "AdminQueue", new {message.Queue.Id}); + + var model = New.ViewModel().Message(message).ReturnUrl(returnUrl); + return View(model); + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminPriorityController.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminPriorityController.cs new file mode 100644 index 000000000..5af2d09ab --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminPriorityController.cs @@ -0,0 +1,86 @@ +using System.Linq; +using System.Web.Mvc; +using Orchard.DisplayManagement; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Messaging.Extensions; +using Orchard.Messaging.Services; +using Orchard.Messaging.ViewModels; +using Orchard.UI.Admin; +using Orchard.UI.Notify; + +namespace Orchard.Messaging.Controllers { + [OrchardFeature("Orchard.Messaging.Queuing")] + [Admin] + public class AdminPriorityController : Controller { + private readonly IMessageQueueManager _messageQueueManager; + private readonly INotifier _notifier; + + public AdminPriorityController(IMessageQueueManager messageQueueManager, IShapeFactory shapeFactory, INotifier notifier) { + _messageQueueManager = messageQueueManager; + _notifier = notifier; + New = shapeFactory; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + public dynamic New { get; set; } + + public ActionResult Index() { + var priorities = _messageQueueManager.GetPriorities().ToList(); + var model = New.ViewModel().Priorities(priorities); + return View(model); + } + + [HttpPost] + public ActionResult Delete(int id) { + var priority = _messageQueueManager.GetPriority(id); + _messageQueueManager.DeletePriority(priority); + _notifier.Information(T("That priority has been deleted.")); + return RedirectToAction("Index"); + } + + public ActionResult Create() { + return View(new MessagePriorityViewModel()); + } + + [HttpPost] + public ActionResult Create(MessagePriorityViewModel model) { + if (!ModelState.IsValid) + return View(model); + + var priority = _messageQueueManager.CreatePriority(model.Name.TrimSafe(), model.DisplayText.TrimSafe(), model.Value); + _notifier.Information(T("Your Priority has been created.")); + return RedirectToAction("Edit", new { priority.Id }); + } + + public ActionResult Edit(int id) { + var priority = _messageQueueManager.GetPriority(id); + if (priority == null || priority.Archived) + return HttpNotFound(); + + var model = new MessagePriorityViewModel { + Id = priority.Id, + Name = priority.Name, + DisplayText = priority.DisplayText, + Value = priority.Value + }; + + return View(model); + } + + [HttpPost] + public ActionResult Edit(MessagePriorityViewModel model) { + if (!ModelState.IsValid) + return View(model); + + var priority = _messageQueueManager.GetPriority(model.Id); + priority.Name = model.Name.TrimSafe(); + priority.DisplayText = model.DisplayText.TrimSafe(); + priority.Value = model.Value; + + _notifier.Information(T("Your Priority has been updated.")); + return RedirectToAction("Edit", new { priority.Id }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminQueueController.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminQueueController.cs new file mode 100644 index 000000000..37e7df8e3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Controllers/AdminQueueController.cs @@ -0,0 +1,138 @@ +using System.Linq; +using System.Web.Mvc; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Messaging.ViewModels; +using Orchard.Mvc; +using Orchard.UI.Admin; +using Orchard.UI.Navigation; +using Orchard.UI.Notify; + +namespace Orchard.Messaging.Controllers { + [OrchardFeature("Orchard.Messaging.Queuing")] + [Admin] + public class AdminQueueController : Controller { + private readonly IMessageQueueManager _messageQueueManager; + private readonly IOrchardServices _services; + private readonly IMessageQueueProcessor _messageQueueProcessor; + + public AdminQueueController(IMessageQueueManager messageQueueManager, IOrchardServices services, IMessageQueueProcessor messageQueueProcessor) { + _messageQueueManager = messageQueueManager; + _services = services; + _messageQueueProcessor = messageQueueProcessor; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public ActionResult Index() { + var queues = _messageQueueManager.GetQueues().ToList(); + + if (queues.Count == 1) { + return RedirectToAction("List", new {id = queues.First().Id}); + } + + var queueShapes = queues.Select(x => _services.New.Queue(x) + .Pending(_messageQueueManager.CountMessages(x.Id, QueuedMessageStatus.Pending)) + .Faulted(_messageQueueManager.CountMessages(x.Id, QueuedMessageStatus.Faulted)) + .Sent(_messageQueueManager.CountMessages(x.Id, QueuedMessageStatus.Sent))).ToList(); + + var model = _services.New.ViewModel() + .Queues(queueShapes); + + return View(model); + } + + public ActionResult Edit(int id, string returnUrl) { + var queue = _messageQueueManager.GetQueue(id); + var model = new MessageQueueViewModel { + Id = queue.Id, + Name = queue.Name, + ReturnUrl = returnUrl + }; + return View(model); + } + + [HttpPost] + public ActionResult Edit(MessageQueueViewModel model) { + if (!ModelState.IsValid) + return View(model); + + CreateOrUpdateQueue(model); + _services.Notifier.Information(T("Your queue has been updated.")); + return Url.IsLocalUrl(model.ReturnUrl) ? (ActionResult) Redirect(model.ReturnUrl) : RedirectToAction("Edit", new {id = model.Id}); + } + + public ActionResult Create() { + return View(new MessageQueueViewModel()); + } + + [HttpPost] + public ActionResult Create(MessageQueueViewModel model) { + if (!ModelState.IsValid) + return View(model); + + var queue = CreateOrUpdateQueue(model); + _services.Notifier.Information(T("Your queue has been created.")); + return RedirectToAction("Edit", new { id = queue.Id }); + } + + public ActionResult List(int id, MessagesFilter filter, PagerParameters pagerParameters) { + var pager = new Pager(_services.WorkContext.CurrentSite, pagerParameters); + var queue = _messageQueueManager.GetQueue(id); + + if (queue == null) + return HttpNotFound(); + + var messageCount = _messageQueueManager.CountMessages(queue.Id, filter.Status); + var messages = _messageQueueManager.GetMessages(queue.Id, filter.Status, pager.GetStartIndex(), pager.PageSize).ToList(); + var model = _services.New.ViewModel() + .Pager(_services.New.Pager(pager).TotalItemCount(messageCount)) + .Queue(queue) + .Messages(messages) + .Filter(filter); + + return View(model); + } + + [HttpPost, ActionName("List")] + [FormValueRequired("submit.Filter")] + public ActionResult Filter(int id, QueuedMessageStatus? status) { + return RedirectToAction("List", new {id, status}); + } + + [HttpPost, ActionName("List")] + [FormValueRequired("submit.Resume")] + public ActionResult Resume(int id, QueuedMessageStatus? status) { + var queue = _messageQueueManager.GetQueue(id); + _messageQueueManager.Resume(queue); + _services.Notifier.Information(T("The queue has been resumed.")); + return RedirectToAction("List", new { id, status }); + } + + [HttpPost, ActionName("List")] + [FormValueRequired("submit.Pause")] + public ActionResult Pause(int id, QueuedMessageStatus? status) { + var queue = _messageQueueManager.GetQueue(id); + _messageQueueManager.Pause(queue); + _services.Notifier.Information(T("The queue has been paused.")); + return RedirectToAction("List", new { id, status }); + } + + [HttpPost, ActionName("List")] + [FormValueRequired("submit.Process")] + public ActionResult Process(int id, QueuedMessageStatus? status) { + _messageQueueProcessor.ProcessQueues(); + _services.Notifier.Information(T("Processing has started.")); + return RedirectToAction("List", new { id, status }); + } + + private MessageQueue CreateOrUpdateQueue(MessageQueueViewModel model) { + var queue = _messageQueueManager.GetQueue(model.Id) ?? _messageQueueManager.CreateQueue(); + queue.Name = model.Name; + return queue; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Extensions/StringExtensions.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Extensions/StringExtensions.cs new file mode 100644 index 000000000..44710b8cb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Extensions/StringExtensions.cs @@ -0,0 +1,7 @@ +namespace Orchard.Messaging.Extensions { + public static class StringExtensions { + public static string TrimSafe(this string value) { + return value != null ? value.Trim() : null; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Migrations.cs deleted file mode 100644 index 8f39d72b6..000000000 --- a/src/Orchard.Web/Modules/Orchard.Messaging/Migrations.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Orchard.Data.Migration; - -namespace Orchard.Messaging { - public class Migrations : DataMigrationImpl { - - public int Create() { - - return 1; - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingMigrations.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingMigrations.cs new file mode 100644 index 000000000..ad1ba91f2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingMigrations.cs @@ -0,0 +1,14 @@ +using Orchard.Data.Migration; + +namespace Orchard.Messaging.Migrations { + public class MessagingMigrations : DataMigrationImpl { + + public int Create() { + SchemaBuilder.CreateTable("MessageSettingsPartRecord", table => table + .ContentPartRecord() + .Column("DefaultChannelService", c => c.WithLength(64))); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingQueuingMigrations.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingQueuingMigrations.cs new file mode 100644 index 000000000..82c5233f5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Migrations/MessagingQueuingMigrations.cs @@ -0,0 +1,50 @@ +using System; +using Orchard.Data.Migration; +using Orchard.Environment.Extensions; +using Orchard.Messaging.Services; + +namespace Orchard.Messaging.Migrations { + [OrchardFeature("Orchard.Messaging.Queuing")] + public class MessagingQueuingMigrations : DataMigrationImpl { + private readonly IMessageQueueManager _messageQueueManager; + + public MessagingQueuingMigrations(IMessageQueueManager messageQueueManager) { + _messageQueueManager = messageQueueManager; + } + + public int Create() { + SchemaBuilder.CreateTable("MessagePriority", table => table + .Column("Id", c => c.Identity().PrimaryKey()) + .Column("Value", c => c.NotNull()) + .Column("Name", c => c.WithLength(50)) + .Column("DisplayText", c => c.WithLength(50)) + .Column("Archived", c => c.NotNull()) + .Column("ArchivedUtc")); + + SchemaBuilder.CreateTable("MessageQueueRecord", table => table + .Column("Id", c => c.Identity().PrimaryKey()) + .Column("Name", c => c.WithLength(50)) + .Column("Status", c => c.WithLength(50)) + .Column("StartedUtc") + .Column("EndedUtc")); + + SchemaBuilder.CreateTable("QueuedMessageRecord", table => table + .Column("Id", c => c.Identity().PrimaryKey()) + .Column("QueueId", c => c.NotNull()) + .Column("Priority_Id") + .Column("ChannelName", c => c.WithLength(50)) + .Column("Recipients", c => c.Unlimited()) + .Column("Payload", c => c.Unlimited()) + .Column("Status", c => c.WithLength(50)) + .Column("CreatedUtc") + .Column("StartedUtc") + .Column("CompletedUtc") + .Column("Result", c => c.Unlimited())); + + _messageQueueManager.CreateDefaultQueue(); + _messageQueueManager.CreateDefaultPriorities(); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessagePriority.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessagePriority.cs new file mode 100644 index 000000000..b9314fde7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessagePriority.cs @@ -0,0 +1,16 @@ +using System; + +namespace Orchard.Messaging.Models { + public class MessagePriority { + public virtual int Id { get; set; } + public virtual int Value { get; set; } + public virtual string Name { get; set; } + public virtual string DisplayText { get; set; } + public virtual bool Archived { get; set; } + public virtual DateTime? ArchivedUtc { get; set; } + + public override string ToString() { + return String.Format("{0} - {1}", Value, Name); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueue.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueue.cs new file mode 100644 index 000000000..b0a5e9bc5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueue.cs @@ -0,0 +1,39 @@ +using System; + +namespace Orchard.Messaging.Models { + public class MessageQueue { + public MessageQueue(MessageQueueRecord record) { + Record = record; + } + + public MessageQueueRecord Record { get; private set; } + + public int Id { + get { return Record.Id; } + } + + public string Name { + get { return Record.Name; } + set { Record.Name = value; } + } + + public MessageQueueStatus Status { + get { return Record.Status; } + internal set { Record.Status = value; } + } + + public DateTime? StartedUtc { + get { return Record.StartedUtc; } + internal set { Record.StartedUtc = value; } + } + + public DateTime? EndedUtc { + get { return Record.EndedUtc; } + internal set { Record.EndedUtc = value; } + } + + public override string ToString() { + return String.Format("{0} - {1}", Name, Status); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueRecord.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueRecord.cs new file mode 100644 index 000000000..834c4d3bc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueRecord.cs @@ -0,0 +1,15 @@ +using System; + +namespace Orchard.Messaging.Models { + public class MessageQueueRecord { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual MessageQueueStatus Status { get; set; } + public virtual DateTime? StartedUtc { get; set; } + public virtual DateTime? EndedUtc { get; set; } + + public override string ToString() { + return String.Format("{0} - {1}", Name, Status); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueStatus.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueStatus.cs new file mode 100644 index 000000000..27c29fd5f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageQueueStatus.cs @@ -0,0 +1,7 @@ +namespace Orchard.Messaging.Models { + public enum MessageQueueStatus { + Idle, + Processing, + Paused + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageRecipient.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageRecipient.cs new file mode 100644 index 000000000..5a9b5bd97 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/MessageRecipient.cs @@ -0,0 +1,15 @@ +namespace Orchard.Messaging.Models { + public class MessageRecipient { + public string AddressOrAlias { get; set; } + + public MessageRecipient() {} + + public MessageRecipient(string addressOrAlias) { + AddressOrAlias = addressOrAlias; + } + + public override string ToString() { + return AddressOrAlias; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessage.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessage.cs new file mode 100644 index 000000000..98d295234 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessage.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Orchard.Messaging.Services; + +namespace Orchard.Messaging.Models { + public class QueuedMessage { + // ReSharper disable InconsistentNaming + public Lazy QueueField; + public Lazy ChannelField; + public Lazy> RecipientsField; + // ReSharper restore InconsistentNaming + + public QueuedMessage(QueuedMessageRecord record) { + Record = record; + } + + public QueuedMessageRecord Record { + get; private set; + } + + public int Id { + get { return Record.Id; } + } + + public MessagePriority Priority { + get { return Record.Priority; } + set { Record.Priority = value; } + } + + public QueuedMessageStatus Status { + get { return Record.Status; } + internal set { Record.Status = value; } + } + + public string Result { + get { return Record.Result; } + set { Record.Result = value; } + } + + public DateTime CreatedUtc { + get { return Record.CreatedUtc; } + } + + public DateTime? StartedUtc { + get { return Record.StartedUtc; } + internal set { Record.StartedUtc = value; } + } + + public DateTime? CompletedUtc { + get { return Record.CompletedUtc; } + internal set { Record.CompletedUtc = value; } + } + + public MessageQueue Queue { + get { return QueueField.Value; } + } + + public IMessageChannel Channel { + get { return ChannelField.Value; } + } + + public IEnumerable Recipients { + get { return RecipientsField.Value; } + } + + public T GetPayload() { + return Record.Payload != null ? JsonConvert.DeserializeObject(Record.Payload) : default(T); + } + + public void SetPayload(T value) { + Record.Payload = !ReferenceEquals(value, default(T)) ? JsonConvert.SerializeObject(value) : null; + } + + public override string ToString() { + return String.Format("Recipients: {0}", String.Join(", ", Recipients.Select(x => x.ToString()))); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageRecord.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageRecord.cs new file mode 100644 index 000000000..6d2474e73 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageRecord.cs @@ -0,0 +1,25 @@ +using System; +using Orchard.Data.Conventions; + +namespace Orchard.Messaging.Models { + public class QueuedMessageRecord { + public virtual int Id { get; set; } + public virtual int QueueId { get; set; } + public virtual MessagePriority Priority { get; set; } + public virtual string ChannelName { get; set; } + + [StringLengthMax] + public virtual string Recipients { get; set; } + + [StringLengthMax] + public virtual string Payload { get; set; } + + public virtual QueuedMessageStatus Status { get; set; } + public virtual DateTime CreatedUtc { get; set; } + public virtual DateTime? StartedUtc { get; set; } + public virtual DateTime? CompletedUtc { get; set; } + + [StringLengthMax] + public virtual string Result { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageStatus.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageStatus.cs new file mode 100644 index 000000000..20d04e7f1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Models/QueuedMessageStatus.cs @@ -0,0 +1,8 @@ +namespace Orchard.Messaging.Models { + public enum QueuedMessageStatus { + Pending, + Sending, + Sent, + Faulted + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Module.txt b/src/Orchard.Web/Modules/Orchard.Messaging/Module.txt index d5e23bbb5..4d6123e48 100644 --- a/src/Orchard.Web/Modules/Orchard.Messaging/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Module.txt @@ -4,7 +4,14 @@ Author: The Orchard Team Website: http://orchardproject.net Version: 1.7.2 OrchardVersion: 1.7.2 -Description: This module provides the messaging infrastructure that module such as Workflows can use. It is only useful as a dependency for other modules that implement specific channels, such as e-mail or Twitter. -FeatureDescription: Messaging services. +Description: This module provides the messaging infrastructure that modules such as Workflows can use. It is only useful as a dependency for other modules that implement specific channels, such as e-mail or Twitter. Category: Messaging -Dependencies: Settings +Features: + Orchard.Messaging: + Description: Provides the messaging infrastructure that modules such as Workflows can use. It is only useful as a dependency for other modules that implement specific channels, such as e-mail or Twitter. + Dependencies: Settings + Orchard.Messaging.Queuing + Name: Message Queuing + Description: Provides message queuing facilities and APIs, enabling the queing of messages to be processed at regular intervals. Includes support for prioritized messages and templates messages using shapes. + Category: Messaging + Dependencies: Orchard.Messaging, Orchard.Forms, Orchard.Workflows, Orchard.jQuery \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Orchard.Messaging.csproj b/src/Orchard.Web/Modules/Orchard.Messaging/Orchard.Messaging.csproj index 5765d2f71..5697a7145 100644 --- a/src/Orchard.Web/Modules/Orchard.Messaging/Orchard.Messaging.csproj +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Orchard.Messaging.csproj @@ -19,6 +19,7 @@ 4.0 + false @@ -47,6 +48,10 @@ false + + False + ..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll + @@ -64,11 +69,33 @@ + + + + + + + + - + + + + + + + + + + + + + + + @@ -76,6 +103,19 @@ + + + submit.js + + + admin-messaging.css + + + + + + + @@ -83,10 +123,66 @@ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} Orchard.Framework + + {9916839c-39fc-4ceb-a5af-89ca7e87119f} + Orchard.Core + + + {642a49d7-8752-4177-80d6-bfbbcfad3de0} + Orchard.Forms + + + {7059493c-8251-4764-9c1e-2368b8b485bc} + Orchard.Workflows + + + + + + + + + + + + + + + + + + + + + + + + + + + + submit.js + + + + + + + + + + + + + + + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/Web.config new file mode 100644 index 000000000..6e8ebec36 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.js b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.js new file mode 100644 index 000000000..b8787205f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.js @@ -0,0 +1,8 @@ +(function($) { + $(function() { + $("#layout-content").on("click", ".submit-form", function (e) { + $(this).parents("form:first").submit(); + e.preventDefault(); + }); + }); +})(jQuery); \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js new file mode 100644 index 000000000..7f62cf0f0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js @@ -0,0 +1,2 @@ +(function(n){n(function(){n("#layout-content").on("click",".submit-form",function(t){n(this).parents("form:first").submit(),t.preventDefault()})})})(jQuery); +//# sourceMappingURL=submit.min.js.map \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js.map b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js.map new file mode 100644 index 000000000..adcd2c47c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Scripts/submit.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"submit.min.js", +"lineCount":1, +"mappings":"CAAC,QAAQ,CAACA,CAAD,CAAI,CACTA,CAAC,CAAC,QAAQ,CAAA,CAAG,CACTA,CAAC,CAAC,iBAAD,CAAmBC,GAAG,CAAC,OAAO,CAAE,cAAc,CAAE,QAAS,CAACC,CAAD,CAAI,CAC1DF,CAAC,CAAC,IAAD,CAAMG,QAAQ,CAAC,YAAD,CAAcC,OAAO,CAAA,CAAE,CACtCF,CAACG,eAAe,CAAA,CAF0C,CAAvC,CADd,CAAZ,CADQ,EAOX,CAACC,MAAD,CAAQ", +"sources":["submit.js"], +"names":["$","on","e","parents","submit","preventDefault","jQuery"] +} diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Services/IMessageChannel.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Services/IMessageChannel.cs new file mode 100644 index 000000000..5808bd2d5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Services/IMessageChannel.cs @@ -0,0 +1,9 @@ +using System; +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.Services { + public interface IMessageChannel : IDependency, IDisposable { + string Name { get; } + void Send(QueuedMessage message); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageChannelBase.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageChannelBase.cs new file mode 100644 index 000000000..8479e9942 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageChannelBase.cs @@ -0,0 +1,24 @@ +using System; +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.Services { + public abstract class MessageChannelBase : Component, IMessageChannel { + ~MessageChannelBase() { + Dispose(false); + } + + public abstract string Name { get; } + public abstract void Send(QueuedMessage message); + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public override string ToString() { + return Name; + } + + protected virtual void Dispose(bool disposing) { } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueBackgroundTask.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueBackgroundTask.cs new file mode 100644 index 000000000..5f4cb8b18 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueBackgroundTask.cs @@ -0,0 +1,16 @@ +using Orchard.Environment.Extensions; +using Orchard.Tasks; + +namespace Orchard.Messaging.Services { + [OrchardFeature("Orchard.Messaging.Queuing")] + public class MessageQueueBackgroundTask : Component, IBackgroundTask { + private readonly IMessageQueueProcessor _messageQueueProcessor; + public MessageQueueBackgroundTask(IMessageQueueProcessor messageQueueProcessor) { + _messageQueueProcessor = messageQueueProcessor; + } + + public void Sweep() { + _messageQueueProcessor.ProcessQueues(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueManager.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueManager.cs new file mode 100644 index 000000000..cee7e101a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueManager.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Orchard.Data; +using Orchard.Environment.Extensions; +using Orchard.Messaging.Models; +using Orchard.Services; + +namespace Orchard.Messaging.Services { + public interface IMessageQueueManager : IDependency { + QueuedMessage Send(MessageRecipient recipient, string channelName, T payload, MessagePriority priority = null, int? queueId = null); + QueuedMessage Send(IEnumerable recipients, string channelName, T payload, MessagePriority priority = null, int? queueId = null); + QueuedMessage Send(string recipient, string channelName, T payload, MessagePriority priority = null, int? queueId = null); + QueuedMessage Send(IEnumerable recipients, string channelName, T payload, MessagePriority priority = null, int? queueId = null); + MessageQueue GetQueue(int id); + MessageQueue GetDefaultQueue(); + MessagePriority GetPriority(int id); + MessagePriority GetPriority(string name); + IEnumerable GetPriorities(); + MessagePriority GetDefaultPriority(); + IEnumerable CreateDefaultPriorities(); + void DeletePriority(MessagePriority priority); + IEnumerable GetIdleQueues(); + void EnterProcessingStatus(MessageQueue queue); + void ExitProcessingStatus(MessageQueue queue); + IEnumerable GetQueues(); + int CountMessages(int queueId, QueuedMessageStatus? status = null); + IQueryable GetMessages(int queueId, QueuedMessageStatus? status = null, int startIndex = 0, int pageSize = 10); + IEnumerable GetPendingMessages(int queueId, int pageSize = 10); + QueuedMessage GetMessage(int id); + MessageQueue CreateQueue(); + MessageQueue CreateDefaultQueue(); + IMessageChannel GetChannel(string name); + IEnumerable GetChannels(); + void Resume(MessageQueue queue); + void Pause(MessageQueue queue); + MessagePriority CreatePriority(string name, string displayText, int rank); + } + + [OrchardFeature("Orchard.Messaging.Queuing")] + public class MessageQueueManager : IMessageQueueManager { + private readonly IClock _clock; + private readonly IRepository _queueRepository; + private readonly IRepository _messageRepository; + private readonly IRepository _priorityRepository; + + public MessageQueueManager( + IClock clock, + IRepository queueRepository, + IRepository messageRepository, + IRepository priorityRepository, + IEnumerable channels) { + _clock = clock; + _queueRepository = queueRepository; + _messageRepository = messageRepository; + _priorityRepository = priorityRepository; + ChannelsDictionary = channels.ToDictionary(x => x.Name); + } + + public QueuedMessage Send(MessageRecipient recipient, string channelName, T payload, MessagePriority priority = null, int? queueId = null) { + return Send(new[] {recipient}, channelName, payload, priority, queueId); + } + + public QueuedMessage Send(string recipient, string channelName, T payload, MessagePriority priority = null, int? queueId = null) { + return Send(new[] { recipient }, channelName, payload, priority, queueId); + } + + public QueuedMessage Send(IEnumerable recipients, string channelName, T payload, MessagePriority priority = null, int? queueId = null) { + return Send(recipients.Select(x => new MessageRecipient(x)), channelName, payload, priority, queueId); + } + + public QueuedMessage Send(IEnumerable recipients, string channelName, T payload, MessagePriority priority = null, int? queueId = null) { + var queue = queueId != null ? GetQueue(queueId.Value) ?? GetDefaultQueue() : GetDefaultQueue(); + + var queuedMessage = new QueuedMessageRecord { + Payload = ToJson(payload), + Recipients = ToJson(recipients.ToList()), + ChannelName = channelName, + Priority = priority ?? GetDefaultPriority(), + QueueId = queue.Id, + CreatedUtc = _clock.UtcNow, + Status = QueuedMessageStatus.Pending + }; + + _messageRepository.Create(queuedMessage); + + return ActivateMessage(queuedMessage); + } + + public IMessageChannel GetChannel(string name) { + return ChannelsDictionary[name]; + } + + public IEnumerable GetChannels() { + return ChannelsDictionary.Select(x => x.Value); + } + + public void Resume(MessageQueue queue) { + if(queue.Status != MessageQueueStatus.Paused) + throw new InvalidOperationException("Cannot resume a queue that is not paused."); + + queue.Status = MessageQueueStatus.Idle; + } + + public void Pause(MessageQueue queue) { + if (queue.Status == MessageQueueStatus.Paused) + throw new InvalidOperationException("Cannot resume a queue that is already paused."); + + queue.Status = MessageQueueStatus.Paused; + } + + public MessagePriority CreatePriority(string name, string displayText, int value) { + var priority = new MessagePriority { + Name = name, + DisplayText = displayText, + Value = value + }; + _priorityRepository.Create(priority); + return priority; + } + + public IDictionary ChannelsDictionary { get; private set; } + + public MessageQueue GetDefaultQueue() { + return ActivateQueue(_queueRepository.Table.FirstOrDefault() ?? CreateDefaultQueue()); + } + + public MessagePriority GetPriority(int id) { + return _priorityRepository.Get(id); + } + + public MessagePriority GetPriority(string name) { + return _priorityRepository.Get(x => x.Name == name); + } + + public IEnumerable GetPriorities() { + return _priorityRepository.Table.Where(x => !x.Archived).OrderBy(x => x.Value).ToList(); + } + + public MessageQueue GetQueue(int id) { + var record = _queueRepository.Get(id); + return record != null ? ActivateQueue(record) : null; + } + + public MessagePriority GetDefaultPriority() { + return _priorityRepository.Table.OrderBy(x => x.Value).FirstOrDefault() ?? CreateDefaultPriorities().First(); + } + + public IEnumerable CreateDefaultPriorities() { + var priorities = new List { + new MessagePriority { + Name = "Low", + DisplayText = "Low", + Value = 1 + }, + new MessagePriority { + Name = "Normal", + DisplayText = "Normal", + Value = 2 + }, + new MessagePriority { + Name = "High", + DisplayText = "High", + Value = 3 + }, + }; + + foreach (var priority in priorities) { + _priorityRepository.Create(priority); + } + + return priorities; + } + + public void DeletePriority(MessagePriority priority) { + priority.Archived = true; + priority.ArchivedUtc = _clock.UtcNow; + } + + public IEnumerable GetIdleQueues() { + return _queueRepository.Table.Where(x => x.Status == MessageQueueStatus.Idle).Select(x => ActivateQueue(x)); + } + + public void EnterProcessingStatus(MessageQueue queue) { + if(queue == null) throw new ArgumentNullException("queue"); + if (queue.Status == MessageQueueStatus.Paused) throw new InvalidOperationException("Cannot process a paused queue. Think about it."); + if (queue.Status == MessageQueueStatus.Processing) throw new InvalidOperationException("Cannot process an already processing queue. What's the point?"); + + queue.Status = MessageQueueStatus.Processing; + queue.StartedUtc = _clock.UtcNow; + } + + public void ExitProcessingStatus(MessageQueue queue) { + if(queue == null) throw new ArgumentNullException("queue"); + if(queue.Status == MessageQueueStatus.Paused) throw new InvalidOperationException("Cannot stop a paused queue."); + if(queue.Status == MessageQueueStatus.Idle) throw new InvalidOperationException("Only processing queues can be stopped."); + + queue.Status = MessageQueueStatus.Idle; + queue.EndedUtc = _clock.UtcNow; + } + + public IEnumerable GetQueues() { + return _queueRepository.Table.Select(ActivateQueue); + } + + public int CountMessages(int queueId, QueuedMessageStatus? status = null) { + return GetMessagesQuery(queueId, status).Count(); + } + + public IQueryable GetMessages(int queueId, QueuedMessageStatus? status = null, int startIndex = 0, int pageSize = 10) { + return GetMessagesQuery(queueId, status).Skip(startIndex).Take(pageSize).Select(ActivateMessage).AsQueryable(); + } + + public QueuedMessage GetMessage(int id) { + return ActivateMessage(_messageRepository.Get(id)); + } + + public MessageQueue CreateQueue() { + var record = new MessageQueueRecord { + Status = MessageQueueStatus.Idle + }; + _queueRepository.Create(record); + return ActivateQueue(record); + } + + MessageQueue IMessageQueueManager.CreateDefaultQueue() { + var queue = CreateQueue(); + queue.Name = "Default"; + return queue; + } + + public IQueryable GetMessagesQuery(int queueId, QueuedMessageStatus? status = null) { + var query = _messageRepository.Table.Where(x => x.QueueId == queueId); + + if (status != null) + query = query.Where(x => x.Status == status.Value); + + query = query.OrderByDescending(x => x.CreatedUtc); + + return query; + } + + public IEnumerable GetPendingMessages(int queueId, int pageSize = 10) { + return _messageRepository.Table + .Where(x => x.Status == QueuedMessageStatus.Pending && x.QueueId == queueId) + .OrderBy(x => x.Priority.Value) + .ThenBy(x => x.CreatedUtc) + .Take(pageSize) + .Select(ActivateMessage) + .ToList(); + } + + private QueuedMessage ActivateMessage(QueuedMessageRecord record) { + return new QueuedMessage(record) { + QueueField = new Lazy(() => GetQueue(record.QueueId)), + RecipientsField = new Lazy>(() => ParseRecipients(record.Recipients)), + ChannelField = new Lazy(() => GetChannel(record.ChannelName)) + }; + } + + private MessageQueue ActivateQueue(MessageQueueRecord record) { + return new MessageQueue(record); + } + + private static IEnumerable ParseRecipients(string data) { + return String.IsNullOrWhiteSpace(data) ? Enumerable.Empty() : JsonConvert.DeserializeObject>(data); + } + + private MessageQueueRecord CreateDefaultQueue() { + var queue = new MessageQueueRecord { + Name = "Default", + Status = MessageQueueStatus.Idle, + }; + + _queueRepository.Create(queue); + return queue; + } + + private static string ToJson(object value) { + return value != null ? JsonConvert.SerializeObject(value) : null; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueProcessor.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueProcessor.cs new file mode 100644 index 000000000..bab096c90 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Services/MessageQueueProcessor.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Logging; +using Orchard.Messaging.Models; +using Orchard.Services; + +namespace Orchard.Messaging.Services { + public interface IMessageQueueProcessor : IDependency { + void ProcessQueues(); + } + + public class MessageQueueProcessor : Component, IMessageQueueProcessor { + private readonly IMessageQueueManager _manager; + private readonly IClock _clock; + + public MessageQueueProcessor(IMessageQueueManager manager, IClock clock) { + _manager = manager; + _clock = clock; + } + + public void ProcessQueues() { + var queuesToProcess = GetQueuesToProcess(); + + foreach (var queue in queuesToProcess.AsParallel()) { + ProcessQueue(queue); + } + } + + private IEnumerable GetQueuesToProcess() { + return _manager.GetIdleQueues().ToList(); + } + + private void ProcessQueue(MessageQueue queue) { + _manager.EnterProcessingStatus(queue); + var messages = _manager.GetPendingMessages(queue.Id); + + while (messages.Any()) { + foreach (var message in messages.AsParallel()) { + ProcessMessage(message); + } + messages = _manager.GetPendingMessages(queue.Id); + } + + _manager.ExitProcessingStatus(queue); + } + + private void ProcessMessage(QueuedMessage message) { + var channel = message.Channel; + + message.StartedUtc = _clock.UtcNow; + message.Status = QueuedMessageStatus.Sending; + + Logger.Debug("Sending message ID {0}.", message.Id); + if (!message.Recipients.Any()) { + message.Status = QueuedMessageStatus.Faulted; + message.Result = String.Format("Cannot send message {0} because at least on recipient is required.", message.Id); + Logger.Error(message.Result); + return; + } + try { + channel.Send(message); + message.Status = QueuedMessageStatus.Sent; + Logger.Debug("Sent message ID {0}.", message.Id); + } + catch (Exception e) { + message.Status = QueuedMessageStatus.Faulted; + message.Result = e.ToString(); + Logger.Error(e, "An unexpected error while sending message {0}. Error message: {1}.", message.Id, e); + } + finally { + message.CompletedUtc = _clock.UtcNow; + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/menu.messaging.png b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/menu.messaging.png new file mode 100644 index 0000000000000000000000000000000000000000..3cbaf68f407bdb6e892d66410480771a5acd3cef GIT binary patch literal 1184 zcmaJ>ZD`zN9FMxXwxeY-Q8yK{Y<1nsUGgNCyCn2h)4Xgarmfe*o_?`u^4wi$lP4z6 z?b15Y4G{-^5QZX*z4%2bh*Q7p!#<1+HV_d8`lS}#5C@85I$={B44<@j)em)nC(lcM zzu*7={9kfpd~{!5@0MPQqWVhv^)i_^lEHO9NZyaV{P!f8He;iTCr}Ny>4u6 z^6fe-L))Eu}^AVhwAFVj^Gmh*s!+YtWL?Hq`#5SGyvq7vRMP^x7A?x&*W$0xH zo+&abPF1aOT0;?}#jKcdIFO?Yau$dnCxJaQ&jEqu1QrMxz$=`r0FiD#46zouHKnYZ zZChklWEvQU3d^=yt!zukqNvUSS(aIjXL&wD5Se%`z;==e;%yxT9mY=Nh1f#@ohsVX zsELaV@pR=1erQ>%!a>{)6bTud*dYtD9P9h(y_V52F2jG_SQ8yr=0eDpVT_uQL-JAE z)*+MF-P=$KB;F`vkw=nZ&+5o&`Y^yHy~vO+S=VzF*#M@@%Nj2ls=)&=!f|;?1(GPs zhAfMqqp=p(G$m73HBm6kJOGBE=6Il!F=FaKmh^mwTMA-q2M+Am^@!azH+N62qD9cg zD5@Yd+o^!@2Er(Appe!k^7P=O7r3Yu?@ZIPa$6ln-T~;E5%THf_$uBS_H{|uG|?y+ zDxdn!OND}<2|2R>^13AJ64U0oYs9S0mHwB@5@*=7IQ~;Cog-3#>2kI7$!4{AU_d%1 zBCYZ01sjmIx?IxLO7h+H7u5dyLHC;? z;e)Sl*>QgR2kVYrU4Lc$4`}z1zg~Nn`n-N<)4*pv&z_=+S3i0C=Y=tgKmYAD!?4__ qcKV6y)UCnf4s-Kl|2Ntj_fboCr_cPEx%+kc&nOwA`llmPhyMX9CxfE^ literal 0 HcmV?d00001 diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/message-status.png b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/message-status.png new file mode 100644 index 0000000000000000000000000000000000000000..683da393f0b77f82043a2de81f58a7f8e3a8c303 GIT binary patch literal 3047 zcmaJ@2{@E%8y@1I66a{KgwGgCn1!*-Ft%Ax*%g&@7&C*3SL0D=T>szyUDx-$&-Z@!eLv6pzW4iH-wAtLOUbqJYe688q_q{! zQPAoLZoc?$f@gt#d8eR}%4v;ZAALKgv)p)%;esv^;o5x~KK1(kjs0*(1Ymd^e*O@ax7@rXSU4}$)l^J0mPH|Q7%XQ7!|(en+WRm#47Lx031G3n zb{8t0%;2&$zQAp4(AIP|he#(;tZ^8yKmbamlF_c7bYkMwz{tWSm*Cu0`shfKU^z#`QdvCU0`;Wz|~zfUJ4+PxSKW3+?n@u zvOm~wV~e8Ln;E6o^Gp7Le#_d-=ZWZ$nn z(=}+7UB%^IpMNsgK3NniUM5T%UV|pn z^h?jR%^9cVmAbKE4NliyB{n{Oyz%f#WX)8z`=>jDus2m>50vn`dBRE(u@OK%++!*% zY$Uf+zBfZPxY{Iicn@8LBDSttjgXg)K?QHxf32V!)%9CNhsX!{r-@d#E!9X~_l4c) z#m?kh_GQtdap=m?20}1H zGV|2D7Ja#s8WGhlsF;6x=x;nT&}o)1~53)L8BDj+n%p@;%9%^ zc_(DL$>^NyRvkX2Q6{Iyv#LDzk4QRp=}61m+?>6$b9i#{ABGtNI~QfAOW93CxpF?m zs3xeq;+6kXW1khOr=R+bB2+}t-?dU6UiUI3;|STy(DNx9&qtb6*g}KT@}jRl=(!l> zenIT$|5`A0TuC-Nt4@1Vw-=5q( z+_^7im^YJHIP>7*_$$%MO61+_zIVICIu6ukuC!8T=Pz3OshXCBHqQ03EtEPnMP_@U&F;tNX`ahjvNu&%Y}{pJP|()-OVHAmH@EHp=bM4yq( zd_)xm5M{Gr0ccb*>ECaL~pWSWh` z3*)wuBky9$lWnbUeDr)(VaFgP=DD{Vc$!$6{>ZdUNH@j1aipBctlVKHNnM2~le#@nH({HqyX_AF%d%BHn@4T>$V&2d#hBiD_tz7?AD2n_zOQ$QD&2KLX!#5wsblt0 zL8g<}iZ-7_8esK2$C4oa?Dn&k@$7Z})4hD@`%&?ya-LskUmyL9t5Q}}`Jx7rKa@hG z9gM6Wc~lCC!JlB9H||utUAD4#xoFHSuvz_%n77xh4<+o@PZvzUyO35g;N(c(2meVcB& ziZrV>gRq=Y&@R7A%PDWs^Mx*}R<>GD>Yjw(c)H_M-To&V@lVf%jvWZe+fKTCaF4Bg;Urog zU1pul`<&yDyaBUtuW#8-e(cCUtnxJs$r&3IUJn8h{WQ6!w4_#ly{+-KxGNSO$NmE_ C_aHF< literal 0 HcmV?d00001 diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/queue-controls.png b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Images/queue-controls.png new file mode 100644 index 0000000000000000000000000000000000000000..b699c1308c466165024f2eff0cc0a839f77288db GIT binary patch literal 1183 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?>3^Ln2Bde0{8v^KDjP)EVYz|dIVz*yhVSl7_V%D~df&{P2mlz_GsrKDK}xwt{?0`hE?GD=Dctn~HE z%ggo3jrH=2()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij`p|xTF>*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09svw}u=W?o8u zd9fx~xv#I4XI^nhVqS8pr;Du;&;Y&6%oHmVQ$u5CGYey5Qx_LQLsw%ba|1^UQ*$>r zOEVV}R||8PUYGpj(%jU%5}4i;gkBSzdO;~6w*Y9fOKMSOS!#+~QGTuh*vD3xxZPri z(>$o&6x?nx!l_pu=oo!ayduRjObD2GKumb51#;l&J~a=R*o%OvI#FR3Hv9m;`#N4ktVcMxZ<`Svfp8U*sn;^EP zWoQ4MB?~6Ji_KBz`e5kBR1iLSPeIU>^k7HN?ab!=Ea7og%r-}apUC~QSzx_(6>t5K zu6LU)o7pdkHdeN}&8nGxsJi-j!n39ATlpqu&FMce_e9BK87(26iH%zH)6{9Tt%ti3Y{c~cTKdd}>FaOXF6RfnF<(3QM6@~Y@ y74nO|n^u_z>8^ca>+`8|`7CDdCr9%c*%%y~l?*Q?Jedb7GCW=VT-G@yGywqY&67d^ literal 0 HcmV?d00001 diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Web.config new file mode 100644 index 000000000..6e8ebec36 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.css b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.css new file mode 100644 index 000000000..a8dbe7945 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.css @@ -0,0 +1,69 @@ +.message-status { + background: url('./images/message-status.png') no-repeat -17px -17px; + padding-left: 25px; + line-height: 16px; + vertical-align: middle; +} + +.message-status.pending { + background-position: -17px -17px; +} + +.message-status.sending { + background-position: -17px -67px; +} + +.message-status.sent { + background-position: -17px -116px; +} + +.message-status.faulted { + background-position: -17px -166px; +} + +fieldset.summary ul li:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +fieldset.summary ul li label, +fieldset.summary ul li span { + float: left; + display: block; +} + +fieldset.summary ul li label { + width: 200px; + font-weight: bold; +} + +fieldset.summary ul li label:after { + content: ":"; +} + +span.queue-status { + margin-right: 1.5em; +} + +table.queues { + clear: both; +} + +table.queues td { + vertical-align: middle; +} + +ul.horizontal li { + float: left; +} + +.clear { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.min.css b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.min.css new file mode 100644 index 000000000..ba19b5acb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/admin-messaging.min.css @@ -0,0 +1 @@ +.message-status{background:url('./images/message-status.png') no-repeat -17px -17px;padding-left:25px;line-height:16px;vertical-align:middle}.message-status.pending{background-position:-17px -17px}.message-status.sending{background-position:-17px -67px}.message-status.sent{background-position:-17px -116px}.message-status.faulted{background-position:-17px -166px}fieldset.summary ul li:after{content:".";display:block;height:0;clear:both;visibility:hidden}fieldset.summary ul li label,fieldset.summary ul li span{float:left;display:block}fieldset.summary ul li label{width:200px;font-weight:bold}fieldset.summary ul li label:after{content:":"}span.queue-status{margin-right:1.5em}table.queues{clear:both}table.queues td{vertical-align:middle}ul.horizontal li{float:left}.clear{content:".";display:block;height:0;clear:both;visibility:hidden} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/menu.messaging-admin.css b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/menu.messaging-admin.css new file mode 100644 index 000000000..502230cd5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/menu.messaging-admin.css @@ -0,0 +1,7 @@ +.navicon-messaging { + background-image: url(./images/menu.messaging.png) !important; +} + +.navicon-messaging:hover { + background-position: 0 -30px !important; +} diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Styles/workflows-activity-sendmessage.css b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/workflows-activity-sendmessage.css new file mode 100644 index 000000000..52b10807c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Styles/workflows-activity-sendmessage.css @@ -0,0 +1,17 @@ +.envelope { + width: 36px; + height: 36px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAARhJREFUSEvtlkEORTAQhnsnSxsre+dwBCdwBidwAyewl0jsbJ3Ahsi8jvfK6JsnqGLxFp/IaGf+jr9NBQDcChu8kumlbVsIggAcx7GK53mAtRYChmEQURSxE2wQhiF0XTcL6PteXLF6xacLD+oAkmUZO9gGaZrKkpoJkSRJ2AlnEsexLDXXXAhAbIrQiyPjA01YFIV8fQfLsmQTmJDn+ZS/qqqlCdUuoArPMiY1HIId/toFdBu6rgt1XcuweTfoqpumAd/3x/iqAAV16t5zQj/t9B22SQAZ+DMRxxbhmwUoaFLaSor+69bE7haAcGZS3/aa95AABTWWzlbDGglAaALT+YcSmPIX8DwBj7iQyC7cdym9EzZ4HSBeOGnowJmFUWAAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: center; +} + +.toolbox-task.toolbox-send-message { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAARhJREFUSEvtlkEORTAQhnsnSxsre+dwBCdwBidwAyewl0jsbJ3Ahsi8jvfK6JsnqGLxFp/IaGf+jr9NBQDcChu8kumlbVsIggAcx7GK53mAtRYChmEQURSxE2wQhiF0XTcL6PteXLF6xacLD+oAkmUZO9gGaZrKkpoJkSRJ2AlnEsexLDXXXAhAbIrQiyPjA01YFIV8fQfLsmQTmJDn+ZS/qqqlCdUuoArPMiY1HIId/toFdBu6rgt1XcuweTfoqpumAd/3x/iqAAV16t5zQj/t9B22SQAZ+DMRxxbhmwUoaFLaSor+69bE7haAcGZS3/aa95AABTWWzlbDGglAaALT+YcSmPIX8DwBj7iQyC7cdym9EzZ4HSBeOGnowJmFUWAAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: 5px 5px; +} + +.toolbox-task.toolbox-send-message div { + margin-left: 36px; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueProcessorTests.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueProcessorTests.cs new file mode 100644 index 000000000..f27ec18e5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueProcessorTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Autofac; +using Moq; +using NUnit.Framework; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Tests; + +namespace Orchard.Messaging.Tests { + [TestFixture] + public class MessageQueueProcessorTests : DatabaseEnabledTestsBase { + private List _messages; + + protected override IEnumerable DatabaseTypes { + get { + yield return typeof(MessageQueueRecord); + yield return typeof(MessagePriority); + yield return typeof(QueuedMessageRecord); + } + } + + public override void Register(ContainerBuilder builder) { + var messageManagerMock = new Mock(); + var queue = new MessageQueue(new MessageQueueRecord { + Name = "Default" + }); + + builder.RegisterInstance(messageManagerMock.Object); + builder.RegisterType().As(); + builder.RegisterType().As(); + + var queues = new List {queue}; + _messages = new List { + CreateMessage("Message 1"), + CreateMessage("Message 2") + }; + + messageManagerMock + .Setup(x => x.Send(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null)) + .Callback(() => _clock.Advance(TimeSpan.FromSeconds(1))) + .Returns(new QueuedMessage(new QueuedMessageRecord ())); + messageManagerMock.Setup(x => x.GetIdleQueues()).Returns(queues); + messageManagerMock.Setup(x => x.EnterProcessingStatus(queue)).Callback(() => { + queue.Record.Status = MessageQueueStatus.Processing; + queue.Record.StartedUtc = _clock.UtcNow; + }); + } + + [Test] + public void ProcessingQueueWithEnoughTimeSendsAllMessages() { + var processor = _container.Resolve(); + + processor.ProcessQueues(); + + foreach (var message in _messages) { + Assert.That(message.Status, Is.EqualTo(QueuedMessageStatus.Sent)); + } + } + + private QueuedMessage CreateMessage(string subject) { + return new QueuedMessage(new QueuedMessageRecord {Id = 1, Payload = "some payload data"}) { + ChannelField = new Lazy(() => _container.Resolve(new NamedParameter("simulatedProcessingTime", TimeSpan.FromSeconds(1)), new NamedParameter("clock", _clock))), + RecipientsField = new Lazy>(() => new[]{ new MessageRecipient("recipient@domain.com") }) + }; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueTests.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueTests.cs new file mode 100644 index 000000000..fe2e2379d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/MessageQueueTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autofac; +using NUnit.Framework; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Tests; + +namespace Orchard.Messaging.Tests { + [TestFixture] + public class MessageQueueTests : DatabaseEnabledTestsBase { + protected override IEnumerable DatabaseTypes { + get { + yield return typeof(MessageQueueRecord); + yield return typeof(MessagePriority); + yield return typeof(QueuedMessageRecord); + } + } + + public override void Register(ContainerBuilder builder) { + builder.RegisterType().As(); + } + + [Test] + public void SendingWithoutSpecifyingQueueUsesDefaultQueue() { + var manager = _container.Resolve(); + var createdMessage = manager.Send("john.doe@live.com", "email", "This is a test subject"); + var defaultQueue = manager.GetDefaultQueue(); + Assert.That(createdMessage.Queue.Id, Is.EqualTo(defaultQueue.Id)); + } + + [Test] + public void SendingWithoutExistingPrioritiesCreatesDefaultPriorities() { + var manager = _container.Resolve(); + var createdMessage = manager.Send("john.doe@live.com", "email", "This is a test subject"); + var low = manager.GetPriority("Low"); + var normal = manager.GetPriority("Normal"); + var high = manager.GetPriority("High"); + Assert.That(low, Is.Not.Null); + Assert.That(normal, Is.Not.Null); + Assert.That(high, Is.Not.Null); + } + + [Test] + public void SendingWithoutExplicitPriorityUsesDefaultPriority() { + var manager = _container.Resolve(); + var createdMessage = manager.Send("john.doe@live.com", "email", "This is a test subject"); + var defaultPriority = manager.GetDefaultPriority(); + Assert.That(createdMessage.Priority.Id, Is.EqualTo(defaultPriority.Id)); + } + + [Test] + public void DefaultPriorityIsLowestPriority() { + var manager = _container.Resolve(); + var priorities = manager.CreateDefaultPriorities().ToList(); + var expectedPriority = priorities.OrderByDescending(x => x.Value).First(); + var actualPriority = manager.GetDefaultPriority(); + Assert.That(expectedPriority.Id, Is.EqualTo(actualPriority.Id)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Orchard.Messaging.Tests.csproj b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Orchard.Messaging.Tests.csproj new file mode 100644 index 000000000..8107c5b59 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Orchard.Messaging.Tests.csproj @@ -0,0 +1,97 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {2B4E039D-EDEB-43DD-8F5A-C640035906A9} + Library + Properties + Orchard.Messaging.Tests + Orchard.Messaging.Tests + v4.5 + 512 + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\ + TRACE + prompt + 4 + false + + + + False + ..\..\..\..\..\lib\autofac\Autofac.dll + + + False + ..\..\..\..\..\lib\moq\Moq.dll + + + ..\..\..\..\..\lib\nunit\nunit.framework.dll + + + + + True + ../../../../../lib/sqlce/System.Data.SqlServerCe.dll + + + + + + + + + + + + + + + + + {abc826d4-2fa1-4f2f-87de-e6095f653810} + Orchard.Framework.Tests + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {6F759635-13D7-4E94-BCC9-80445D63F117} + Orchard.Tokens + + + {085948ff-0e9b-4a9a-b564-f8b8b4bdddbc} + Orchard.Messaging + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..240c74946 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Messaging.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("Copyright © Outercurve Foundation 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5ffbf171-1543-4a47-ab9b-de5455f96b2c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.7.1")] +[assembly: AssemblyFileVersion("1.7.1")] diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Tests/StubMessageChannel.cs b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/StubMessageChannel.cs new file mode 100644 index 000000000..7df665fcb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Tests/StubMessageChannel.cs @@ -0,0 +1,26 @@ +using System; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Tests.Stubs; + +namespace Orchard.Messaging.Tests { + public class StubMessageChannel : IMessageChannel { + public const string ChannelName = "stub"; + private readonly TimeSpan _simulatedProcessingTime; + private readonly StubClock _clock; + + public StubMessageChannel(TimeSpan simulatedProcessingTime, StubClock clock) { + _simulatedProcessingTime = simulatedProcessingTime; + _clock = clock; + } + + public void Dispose() { + } + + public string Name { get { return ChannelName; } } + + public void Send(QueuedMessage message) { + _clock.Advance(_simulatedProcessingTime); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagePriorityViewModel.cs b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagePriorityViewModel.cs new file mode 100644 index 000000000..d809cc74b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagePriorityViewModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Messaging.ViewModels { + public class MessagePriorityViewModel { + public int Id { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string DisplayText { get; set; } + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessageQueueViewModel.cs b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessageQueueViewModel.cs new file mode 100644 index 000000000..6ee8b7182 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessageQueueViewModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Messaging.ViewModels { + public class MessageQueueViewModel { + public int Id { get; set; } + + [Required] + public string Name { get; set; } + + public string ReturnUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagesFilter.cs b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagesFilter.cs new file mode 100644 index 000000000..529a41937 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/ViewModels/MessagesFilter.cs @@ -0,0 +1,7 @@ +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.ViewModels { + public class MessagesFilter { + public QueuedMessageStatus? Status { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/Activity-SendMessage.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/Activity-SendMessage.cshtml new file mode 100644 index 000000000..c19c22d39 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/Activity-SendMessage.cshtml @@ -0,0 +1,4 @@ +
+
+
+ diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminMessage/Details.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminMessage/Details.cshtml new file mode 100644 index 000000000..17705b1e1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminMessage/Details.cshtml @@ -0,0 +1,47 @@ +@using Orchard.Messaging.Models +@using Orchard.Utility.Extensions +@{ + Style.Include("admin-messaging.css", "admin-messaging.min.css"); + Layout.Title = T("Message Details"); + var message = (QueuedMessage)Model.Message; + var returnUrl = (string)Model.ReturnUrl; +} +
+
    +
  • + + @message.Status +
  • + @if (message.Status == QueuedMessageStatus.Faulted) { +
  • + + @message.Result +
  • + } +
  • + + @message.Priority +
  • +
  • + + @String.Join(", ", message.Recipients) +
  • +
  • + + @message.CreatedUtc +
  • +
  • + + @message.StartedUtc +
  • +
  • + + @message.CompletedUtc +
  • +
  • + + @message.Record.Payload +
  • +
+
+@T("Back") \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Create.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Create.cshtml new file mode 100644 index 000000000..40e21a3d8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Create.cshtml @@ -0,0 +1,10 @@ +@model Orchard.Messaging.ViewModels.MessagePriorityViewModel +@{ + Layout.Title = T("Create Priority"); +} +@Html.ValidationSummary() +@using (Html.BeginFormAntiForgeryPost()) { + @Html.EditorForModel() + + @T("Cancel") +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Edit.cshtml new file mode 100644 index 000000000..b69bc7bdf --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Edit.cshtml @@ -0,0 +1,9 @@ +@model Orchard.Messaging.ViewModels.MessagePriorityViewModel +@{ + Layout.Title = T("Edit Priority"); +} +@using (Html.BeginFormAntiForgeryPost()) { + @Html.EditorForModel() + + @T("Back") +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Index.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Index.cshtml new file mode 100644 index 000000000..ab0c79c91 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminPriority/Index.cshtml @@ -0,0 +1,40 @@ +@{ + var priorities = (IEnumerable)Model.Priorities; + Script.Require("jQuery").AtFoot(); + Script.Include("submit.js", "submit.min.js").AtFoot(); + Style.Include("admin-messaging.css", "admin-messaging.min.css"); + Layout.Title = T("Manage Priorities"); +} +
+ @Html.ActionLink(T("Create Priority").ToString(), "Create", new { Area = "Orchard.Messaging" }, new { @class = "button primaryAction" }) +
+@if (!priorities.Any()) { +
@T("There are no priorities. Create a new Priority.", Url.Action("Create", "AdminPriority"))
+} +else { + using (Html.BeginFormAntiForgeryPost()) { + + + + + + + + + + + @foreach (var priority in priorities) { + + + + + + + } + +
@T("Name")@T("Display Text")@T("Value")
@priority.Name@priority.DisplayText@priority.Value + @T("Edit")@T(" | ") + @T("Delete") +
+ } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Create.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Create.cshtml new file mode 100644 index 000000000..9a39832e2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Create.cshtml @@ -0,0 +1,9 @@ +@model Orchard.Messaging.ViewModels.MessageQueueViewModel +@{ + Layout.Title = T("Create Queue"); +} +@using (Html.BeginFormAntiForgeryPost()) { + @Html.EditorForModel() + + @T("Cancel") +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Edit.cshtml new file mode 100644 index 000000000..4672f0ce4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Edit.cshtml @@ -0,0 +1,10 @@ +@model Orchard.Messaging.ViewModels.MessageQueueViewModel +@{ + Layout.Title = T("Edit Queue"); + var backUrl = !String.IsNullOrWhiteSpace(Model.ReturnUrl) ? Model.ReturnUrl : Url.Action("Index"); +} +@using (Html.BeginFormAntiForgeryPost()) { + @Html.EditorForModel() + + @T("Back") +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Index.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Index.cshtml new file mode 100644 index 000000000..15f0b7d48 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/Index.cshtml @@ -0,0 +1,62 @@ +@using Orchard.Messaging.Models +@using Orchard.Utility.Extensions +@{ + var queues = (IEnumerable) Model.Queues; + Script.Require("jQuery").AtFoot(); + Script.Include("submit.js", "submit.min.js").AtFoot(); + Style.Include("admin-messaging.css", "admin-messaging.min.css"); + Layout.Title = T("Manage Queues"); +} +
+ @Html.ActionLink(T("Create Queue").ToString(), "Create", new { Area = "Orchard.Messaging" }, new { @class = "button primaryAction" }) +
+@if (!queues.Any()) { +
@T("There are no queues. Create a new Queue.", Url.Action("Create", "AdminQueue"))
+} +else { + + + + + + + + + + + + + + + + + @foreach (var queue in queues) { + + + + + + + + + + + + + } + +
@T("Name")@T("Status")@T("Time Slice")@T("Update Interval")@T("Pending")@T("Sent")@T("Faulted")@T("Last Run")@T("Next Run")
@queue.Name@queue.Status@queue.TimeSlice@queue.UpdateFrequency@queue.Pending@queue.Sent@queue.Faulted@queue.EndedUtc@queue.NextRun +
    +
  • + @if (queue.Status == MessageQueueStatus.Paused) { + using (Html.BeginFormAntiForgeryPost(Url.Action("Resume", new { id = queue.Id }))) { @T("Resume") } + } + @if (queue.Status == MessageQueueStatus.Idle || queue.Status == MessageQueueStatus.Processing) { + using (Html.BeginFormAntiForgeryPost(Url.Action("Pause", new { id = queue.Id }))) { @T("Pause") } + } +
  • +
  •  | @T("Properties")
  • +
+
+
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/List.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/List.cshtml new file mode 100644 index 000000000..129b7531a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/AdminQueue/List.cshtml @@ -0,0 +1,74 @@ +@using Orchard.Messaging.Models +@using Orchard.Messaging.ViewModels +@using Orchard.Utility.Extensions +@{ + var queue = (MessageQueue) Model.Queue; + var messages = (IEnumerable) Model.Messages; + var filter = (MessagesFilter) Model.Filter; + Layout.Title = T("Message Queue"); + Style.Include("admin-messaging.css", "admin-messaging.min.css"); +} +@using (Html.BeginFormAntiForgeryPost()) { +
+ @T("Status: {0}", queue.Status) + @if (queue.Status == MessageQueueStatus.Paused) { + + } + else { + + + } +
+
+ + + +
+if (!messages.Any()) { + if (filter.Status == null) { +
@T("There no messages in this queue yet.")
+ } + else { +
@T("There no messages in the '{0}' status.", filter.Status)
+ } +} +else { + + + + + + + + + + + + + + + @foreach (var message in messages) { + + + + + + + + + + + } + +
@T("Status")@T("Priority")@T("Recipients")@T("Payload")@T("Created")@T("Started")@T("Completed")@T("Actions")
@message.Status@message.Priority.Name@String.Join(", ", message.Recipients).Ellipsize(50)@message.Record.Payload.Ellipsize(100, "...")@message.CreatedUtc@message.StartedUtc@message.CompletedUtc + @T("Details") +
+ } + @Display(Model.Pager) +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessagePriorityViewModel.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessagePriorityViewModel.cshtml new file mode 100644 index 000000000..c2e966f12 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessagePriorityViewModel.cshtml @@ -0,0 +1,17 @@ +@model Orchard.Messaging.ViewModels.MessagePriorityViewModel +
+
+ @Html.HiddenFor(m => m.Id) + @Html.LabelFor(m => m.Name) + @Html.TextBoxFor(m => m.Name, new { @class = "text medium" }) +
+
+ @Html.LabelFor(m => m.DisplayText) + @Html.TextBoxFor(m => m.DisplayText, new { @class = "text medium" }) +
+
+ @Html.LabelFor(m => m.Value) + @Html.TextBoxFor(m => m.Value, new { @class = "text small" }) + @T("The higher the value, the higher the priority.") +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessageQueueViewModel.cshtml b/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessageQueueViewModel.cshtml new file mode 100644 index 000000000..959406ca1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Messaging/Views/EditorTemplates/MessageQueueViewModel.cshtml @@ -0,0 +1,18 @@ +@model Orchard.Messaging.ViewModels.MessageQueueViewModel +
+ @Html.HiddenFor(m => m.Id) +
+ @Html.LabelFor(m => m.Name) + @Html.TextBoxFor(m => m.Name, new { @class = "text medium" }) +
+
+ @Html.LabelFor(m => m.TimeSlice) + @Html.TextBoxFor(m => m.TimeSlice, new { @class = "text medium" }) + @T("The time this queue gets to process per cycle.") +
+
+ @Html.LabelFor(m => m.UpdateFrequency) + @Html.TextBoxFor(m => m.UpdateFrequency, new { @class = "text medium" }) + @T("The interval this queue is to be processed.") +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj index 37943d172..3a03f3f28 100644 --- a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj @@ -53,7 +53,11 @@ 3.5 + + + + False ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll @@ -97,6 +101,14 @@ + + + web.config + + + web.config + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Debug.config b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Debug.config new file mode 100644 index 000000000..2e302f9f9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Release.config b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Release.config new file mode 100644 index 000000000..c35844462 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.Templates/AdminMenu.cs new file mode 100644 index 000000000..9161a0aa8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/AdminMenu.cs @@ -0,0 +1,13 @@ +using Orchard.UI.Navigation; + +namespace Orchard.Templates { + public class AdminMenu : Component, INavigationProvider { + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) { + builder + .AddImageSet("templates") + .Add(T("Templates"), "5.0", item => item.Action("List", "Admin", new { area = "Orchard.Templates", id = "" })); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Templates/Controllers/AdminController.cs new file mode 100644 index 000000000..ea98faeda --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Controllers/AdminController.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.MetaData.Models; +using Orchard.Core.Common.Models; +using Orchard.Core.Contents; +using Orchard.Core.Contents.ViewModels; +using Orchard.Data; +using Orchard.DisplayManagement; +using Orchard.Localization; +using Orchard.Mvc.Extensions; +using Orchard.Settings; +using Orchard.UI.Navigation; +using Orchard.UI.Notify; + +namespace Orchard.Templates.Controllers { + public class AdminController : Controller { + private readonly IContentManager _contentManager; + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly ITransactionManager _transactionManager; + private readonly ISiteService _siteService; + + public AdminController( + IOrchardServices orchardServices, + IContentManager contentManager, + IContentDefinitionManager contentDefinitionManager, + ITransactionManager transactionManager, + ISiteService siteService, + IShapeFactory shapeFactory) { + Services = orchardServices; + _contentManager = contentManager; + _contentDefinitionManager = contentDefinitionManager; + _transactionManager = transactionManager; + _siteService = siteService; + T = NullLocalizer.Instance; + Shape = shapeFactory; + } + + dynamic Shape { get; set; } + public IOrchardServices Services { get; private set; } + public Localizer T { get; set; } + + public ActionResult List(ListContentsViewModel model, PagerParameters pagerParameters) { + var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters); + var query = _contentManager.Query(VersionOptions.Latest, GetShapeTypes().Select(ctd => ctd.Name).ToArray()); + + if (!string.IsNullOrEmpty(model.TypeName)) { + var contentTypeDefinition = _contentDefinitionManager.GetTypeDefinition(model.TypeName); + if (contentTypeDefinition == null) + return HttpNotFound(); + + model.TypeDisplayName = !string.IsNullOrWhiteSpace(contentTypeDefinition.DisplayName) + ? contentTypeDefinition.DisplayName + : contentTypeDefinition.Name; + query = query.ForType(model.TypeName); + } + + switch (model.Options.OrderBy) { + case ContentsOrder.Modified: + query = query.OrderByDescending(cr => cr.ModifiedUtc); + break; + case ContentsOrder.Published: + query = query.OrderByDescending(cr => cr.PublishedUtc); + break; + case ContentsOrder.Created: + query = query.OrderByDescending(cr => cr.CreatedUtc); + break; + } + + model.Options.SelectedFilter = model.TypeName; + model.Options.FilterOptions = GetShapeTypes() + .Select(x => new KeyValuePair(x.Name, x.DisplayName)) + .ToList().OrderBy(x => x.Value); + + var pagerShape = Shape.Pager(pager).TotalItemCount(query.Count()); + var pageOfContentItems = query.Slice(pager.GetStartIndex(), pager.PageSize).ToList(); + var list = Shape.List(); + + list.AddRange(pageOfContentItems.Select(ci => _contentManager.BuildDisplay(ci, "SummaryAdmin"))); + + var viewModel = Shape.ViewModel() + .ContentItems(list) + .Pager(pagerShape) + .Options(model.Options) + .TypeDisplayName(model.TypeDisplayName ?? ""); + + return View(viewModel); + } + + [HttpPost, ActionName("List")] + [Mvc.FormValueRequired("submit.Filter")] + public ActionResult ListFilterPOST(ContentOptions options) { + var routeValues = ControllerContext.RouteData.Values; + if (options != null) { + routeValues["Options.OrderBy"] = options.OrderBy; + if (GetShapeTypes().Any(x => String.Equals(x.Name, options.SelectedFilter, StringComparison.OrdinalIgnoreCase))) { + routeValues["id"] = options.SelectedFilter; + } + else { + routeValues.Remove("id"); + } + } + + return RedirectToAction("List", routeValues); + } + + [HttpPost, ActionName("List")] + [Mvc.FormValueRequired("submit.BulkEdit")] + public ActionResult ListPOST(ContentOptions options, IEnumerable itemIds, string returnUrl) { + if (itemIds != null) { + var checkedContentItems = _contentManager.GetMany(itemIds, VersionOptions.Latest, QueryHints.Empty); + switch (options.BulkAction) { + case ContentsBulkAction.None: + break; + case ContentsBulkAction.PublishNow: + foreach (var item in checkedContentItems) { + if (!Services.Authorizer.Authorize(Permissions.PublishContent, item, T("Couldn't publish selected content."))) { + _transactionManager.Cancel(); + return new HttpUnauthorizedResult(); + } + + _contentManager.Publish(item); + } + Services.Notifier.Information(T("Content successfully published.")); + break; + case ContentsBulkAction.Unpublish: + foreach (var item in checkedContentItems) { + if (!Services.Authorizer.Authorize(Permissions.PublishContent, item, T("Couldn't unpublish selected content."))) { + _transactionManager.Cancel(); + return new HttpUnauthorizedResult(); + } + + _contentManager.Unpublish(item); + } + Services.Notifier.Information(T("Content successfully unpublished.")); + break; + case ContentsBulkAction.Remove: + foreach (var item in checkedContentItems) { + if (!Services.Authorizer.Authorize(Permissions.DeleteContent, item, T("Couldn't remove selected content."))) { + _transactionManager.Cancel(); + return new HttpUnauthorizedResult(); + } + + _contentManager.Remove(item); + } + Services.Notifier.Information(T("Content successfully removed.")); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return this.RedirectLocal(returnUrl, () => RedirectToAction("List")); + } + + public ActionResult Create(string id) { + var types = GetShapeTypes(); + var typeName = String.IsNullOrWhiteSpace(id) ? types.Count() == 1 ? types.First().Name : null : id; + return String.IsNullOrEmpty(typeName) ? CreatableTypeList() : RedirectToAction("Create", "Admin", new {area = "Contents", id = typeName}); + } + + private ActionResult CreatableTypeList() { + var viewModel = Shape.ViewModel(ContentTypes: GetShapeTypes()); + return View("CreatableTypeList", viewModel); + } + + private IEnumerable GetShapeTypes() { + return _contentDefinitionManager.ListTypeDefinitions().Where(x => x.Parts.Any(p => p.PartDefinition.Name == "ShapePart")).ToList(); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Drivers/ShapePartDriver.cs b/src/Orchard.Web/Modules/Orchard.Templates/Drivers/ShapePartDriver.cs new file mode 100644 index 000000000..864b106f3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Drivers/ShapePartDriver.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Compilation.Razor; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Data; +using Orchard.Localization; +using Orchard.Templates.Helpers; +using Orchard.Templates.Models; +using Orchard.Templates.Services; +using Orchard.Templates.ViewModels; +using Orchard.Utility.Extensions; + +namespace Orchard.Templates.Drivers { + public class ShapePartDriver : ContentPartDriver { + private readonly IEnumerable _processors; + private readonly IRazorTemplateCache _cache; + private readonly ITemplateService _service; + private readonly ITransactionManager _transactions; + + public ShapePartDriver(IEnumerable processors, IRazorTemplateCache cache, ITemplateService service,ITransactionManager transactions) { + _processors = processors; + _cache = cache; + _service = service; + _transactions = transactions; + T = NullLocalizer.Instance; + } + + Localizer T { get; set; } + + protected override DriverResult Editor(ShapePart part, dynamic shapeHelper) { + return Editor(part, null, shapeHelper); + } + + protected override DriverResult Editor(ShapePart part, IUpdateModel updater, dynamic shapeHelper) { + var viewModel = new ShapePartViewModel { + Name = part.Name, + Template = part.Template, + Language = part.Language, + AvailableLanguages = _processors.Select(x => x.Type).Distinct().ToArray() + }; + + if (updater != null + && updater.TryUpdateModel(viewModel, Prefix, null, new[] { "AvailableLanguages" }) + && ValidateShapeName(viewModel.Name, updater)) { + part.Name = viewModel.Name.TrimSafe(); + part.Language = viewModel.Language; + part.Template = viewModel.Template; + + try { + var processor = _processors.FirstOrDefault(x => String.Equals(x.Type, part.Language, StringComparison.OrdinalIgnoreCase)); + processor.Verify(part.Template); + _cache.Set(part.Name, part.Template); + } + catch (Exception ex) { + updater.AddModelError("", T("Template processing error: {0}", ex.Message)); + _transactions.Cancel(); + } + } + return ContentShape("Parts_Shape_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts.Shape", Model: viewModel, Prefix: Prefix)); + } + + private bool ValidateShapeName(string name, IUpdateModel updater) { + if (!string.IsNullOrWhiteSpace(name) && + name[0].IsLetter() && + name.All(c => c.IsLetter() || Char.IsDigit(c) || c == '.' || c == '-' )) { + return true; + } + + updater.AddModelError("Name", T("Shape names can only contain alphanumerical, dot (.) or dash (-) characters and have to start with a letter.")); + return false; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Handlers/ShapePartHandler.cs b/src/Orchard.Web/Modules/Orchard.Templates/Handlers/ShapePartHandler.cs new file mode 100644 index 000000000..9206989bc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Handlers/ShapePartHandler.cs @@ -0,0 +1,16 @@ +using Orchard.Caching; +using Orchard.ContentManagement.Handlers; +using Orchard.Data; +using Orchard.Templates.Models; +using Orchard.Templates.Services; + +namespace Orchard.Templates.Handlers { + public class ShapePartHandler : ContentHandler { + public ShapePartHandler(IRepository repository, ISignals signals) { + Filters.Add(StorageFilter.For(repository)); + + OnGetContentItemMetadata((ctx, part) => ctx.Metadata.DisplayText = part.Name); + OnCreated((ctx, part) => signals.Trigger(DefaultTemplateService.TemplatesSignal)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Helpers/StringExtensions.cs b/src/Orchard.Web/Modules/Orchard.Templates/Helpers/StringExtensions.cs new file mode 100644 index 000000000..7cdb76f08 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Helpers/StringExtensions.cs @@ -0,0 +1,7 @@ +namespace Orchard.Templates.Helpers { + public static class StringExtensions { + public static string TrimSafe(this string value) { + return value == null ? null : value.Trim(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Templates/Migrations.cs new file mode 100644 index 000000000..b22d8674f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Migrations.cs @@ -0,0 +1,26 @@ +using Orchard.ContentManagement.MetaData; +using Orchard.Core.Contents.Extensions; +using Orchard.Data.Migration; + +namespace Orchard.Templates { + public class Migrations : DataMigrationImpl { + public int Create() { + SchemaBuilder.CreateTable("ShapePartRecord", table => table + .ContentPartRecord() + .Column("Name", c => c.WithLength(100)) + .Column("Language", c => c.WithLength(50)) + .Column("Template", c => c.Unlimited())); + + ContentDefinitionManager.AlterPartDefinition("ShapePart", part => part + .Attachable() + .WithDescription("Turns a type into a shape provider.")); + + ContentDefinitionManager.AlterTypeDefinition("Template", type => type + .WithPart("CommonPart") + .WithPart("IdentityPart") + .WithPart("ShapePart") + .Draftable()); + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Models/ShapePart.cs b/src/Orchard.Web/Modules/Orchard.Templates/Models/ShapePart.cs new file mode 100644 index 000000000..7a987ce74 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Models/ShapePart.cs @@ -0,0 +1,30 @@ +using Orchard.ContentManagement; +using Orchard.ContentManagement.Records; +using Orchard.Data.Conventions; + +namespace Orchard.Templates.Models { + public class ShapePart : ContentPart { + public string Name { + get { return Record.Name; } + set { Record.Name = value; } + } + + public string Language { + get { return Record.Language; } + set { Record.Language = value; } + } + + public string Template { + get { return Record.Template; } + set { Record.Template = value; } + } + } + + public class ShapePartRecord : ContentPartRecord { + public virtual string Name { get; set; } + public virtual string Language { get; set; } + + [StringLengthMax] + public virtual string Template { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Module.txt b/src/Orchard.Web/Modules/Orchard.Templates/Module.txt new file mode 100644 index 000000000..f70064c9b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Module.txt @@ -0,0 +1,13 @@ +Name: Orchard.Templates +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.0 +OrchardVersion: 1.8 +Description: Provides a Template type that can be used to stre Razor code and used as a shape. +Features: + Orchard.Templates: + Name: Templates + Description: Provides a Template type that represents a Razor template, stored as a content item. + Category: Content + Dependencies: Contents, Orchard.Tokens \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Orchard.Templates.csproj b/src/Orchard.Web/Modules/Orchard.Templates/Orchard.Templates.csproj new file mode 100644 index 000000000..b3994bb8d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Orchard.Templates.csproj @@ -0,0 +1,240 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.Templates + Orchard.Templates + v4.5 + false + + + 4.0 + + + false + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + false + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + false + + + + + + + 3.5 + + + + + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Razor.dll + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.WebPages.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {CDE24A24-01D3-403C-84B9-37722E18DFB7} + Orchard.Themes + + + {6f759635-13d7-4e94-bcc9-80445d63f117} + Orchard.Tokens + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Placement.info b/src/Orchard.Web/Modules/Orchard.Templates/Placement.info new file mode 100644 index 000000000..9e4408cd0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Placement.info @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Templates/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..4515383cd --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Templates")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b37c269a-7c4a-43cd-8385-cb2fa0ded871")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/Web.config new file mode 100644 index 000000000..6e8ebec36 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/emacs.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/emacs.js new file mode 100644 index 000000000..7a3dfb11a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/emacs.js @@ -0,0 +1,387 @@ +(function() { + "use strict"; + + var Pos = CodeMirror.Pos; + function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } + + // Kill 'ring' + + var killRing = []; + function addToRing(str) { + killRing.push(str); + if (killRing.length > 50) killRing.shift(); + } + function growRingTop(str) { + if (!killRing.length) return addToRing(str); + killRing[killRing.length - 1] += str; + } + function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; } + function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); } + + var lastKill = null; + + function kill(cm, from, to, mayGrow, text) { + if (text == null) text = cm.getRange(from, to); + + if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen)) + growRingTop(text); + else + addToRing(text); + cm.replaceRange("", from, to, "+delete"); + + if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()}; + else lastKill = null; + } + + // Boundaries of various units + + function byChar(cm, pos, dir) { + return cm.findPosH(pos, dir, "char", true); + } + + function byWord(cm, pos, dir) { + return cm.findPosH(pos, dir, "word", true); + } + + function byLine(cm, pos, dir) { + return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn); + } + + function byPage(cm, pos, dir) { + return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn); + } + + function byParagraph(cm, pos, dir) { + var no = pos.line, line = cm.getLine(no); + var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch)); + var fst = cm.firstLine(), lst = cm.lastLine(); + for (;;) { + no += dir; + if (no < fst || no > lst) + return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null)); + line = cm.getLine(no); + var hasText = /\S/.test(line); + if (hasText) sawText = true; + else if (sawText) return Pos(no, 0); + } + } + + function bySentence(cm, pos, dir) { + var line = pos.line, ch = pos.ch; + var text = cm.getLine(pos.line), sawWord = false; + for (;;) { + var next = text.charAt(ch + (dir < 0 ? -1 : 0)); + if (!next) { // End/beginning of line reached + if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch); + text = cm.getLine(line + dir); + if (!/\S/.test(text)) return Pos(line, ch); + line += dir; + ch = dir < 0 ? text.length : 0; + continue; + } + if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0)); + if (!sawWord) sawWord = /\w/.test(next); + ch += dir; + } + } + + function byExpr(cm, pos, dir) { + var wrap; + if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true)) + && wrap.match && (wrap.forward ? 1 : -1) == dir) + return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to; + + for (var first = true;; first = false) { + var token = cm.getTokenAt(pos); + var after = Pos(pos.line, dir < 0 ? token.start : token.end); + if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) { + var newPos = cm.findPosH(after, dir, "char"); + if (posEq(after, newPos)) return pos; + else pos = newPos; + } else { + return after; + } + } + } + + // Prefixes (only crudely supported) + + function getPrefix(cm, precise) { + var digits = cm.state.emacsPrefix; + if (!digits) return precise ? null : 1; + clearPrefix(cm); + return digits == "-" ? -1 : Number(digits); + } + + function repeated(cmd) { + var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd; + return function(cm) { + var prefix = getPrefix(cm); + f(cm); + for (var i = 1; i < prefix; ++i) f(cm); + }; + } + + function findEnd(cm, by, dir) { + var pos = cm.getCursor(), prefix = getPrefix(cm); + if (prefix < 0) { dir = -dir; prefix = -prefix; } + for (var i = 0; i < prefix; ++i) { + var newPos = by(cm, pos, dir); + if (posEq(newPos, pos)) break; + pos = newPos; + } + return pos; + } + + function move(by, dir) { + var f = function(cm) { + cm.extendSelection(findEnd(cm, by, dir)); + }; + f.motion = true; + return f; + } + + function killTo(cm, by, dir) { + kill(cm, cm.getCursor(), findEnd(cm, by, dir), true); + } + + function addPrefix(cm, digit) { + if (cm.state.emacsPrefix) { + if (digit != "-") cm.state.emacsPrefix += digit; + return; + } + // Not active yet + cm.state.emacsPrefix = digit; + cm.on("keyHandled", maybeClearPrefix); + cm.on("inputRead", maybeDuplicateInput); + } + + var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true}; + + function maybeClearPrefix(cm, arg) { + if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg)) + clearPrefix(cm); + } + + function clearPrefix(cm) { + cm.state.emacsPrefix = null; + cm.off("keyHandled", maybeClearPrefix); + cm.off("inputRead", maybeDuplicateInput); + } + + function maybeDuplicateInput(cm, event) { + var dup = getPrefix(cm); + if (dup > 1 && event.origin == "+input") { + var one = event.text.join("\n"), txt = ""; + for (var i = 1; i < dup; ++i) txt += one; + cm.replaceSelection(txt, "end", "+input"); + } + } + + function addPrefixMap(cm) { + cm.state.emacsPrefixMap = true; + cm.addKeyMap(prefixMap); + cm.on("keyHandled", maybeRemovePrefixMap); + cm.on("inputRead", maybeRemovePrefixMap); + } + + function maybeRemovePrefixMap(cm, arg) { + if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return; + cm.removeKeyMap(prefixMap); + cm.state.emacsPrefixMap = false; + cm.off("keyHandled", maybeRemovePrefixMap); + cm.off("inputRead", maybeRemovePrefixMap); + } + + // Utilities + + function setMark(cm) { + cm.setCursor(cm.getCursor()); + cm.setExtending(true); + cm.on("change", function() { cm.setExtending(false); }); + } + + function getInput(cm, msg, f) { + if (cm.openDialog) + cm.openDialog(msg + ": ", f, {bottom: true}); + else + f(prompt(msg, "")); + } + + function operateOnWord(cm, op) { + var start = cm.getCursor(), end = cm.findPosH(start, 1, "word"); + cm.replaceRange(op(cm.getRange(start, end)), start, end); + cm.setCursor(end); + } + + function toEnclosingExpr(cm) { + var pos = cm.getCursor(), line = pos.line, ch = pos.ch; + var stack = []; + while (line >= cm.firstLine()) { + var text = cm.getLine(line); + for (var i = ch == null ? text.length : ch; i > 0;) { + var ch = text.charAt(--i); + if (ch == ")") + stack.push("("); + else if (ch == "]") + stack.push("["); + else if (ch == "}") + stack.push("{"); + else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch)) + return cm.extendSelection(Pos(line, i)); + } + --line; ch = null; + } + } + + // Actual keymap + + var keyMap = CodeMirror.keyMap.emacs = { + "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));}, + "Ctrl-K": repeated(function(cm) { + var start = cm.getCursor(), end = cm.clipPos(Pos(start.line)); + var text = cm.getRange(start, end); + if (!/\S/.test(text)) { + text += "\n"; + end = Pos(start.line + 1, 0); + } + kill(cm, start, end, true, text); + }), + "Alt-W": function(cm) { + addToRing(cm.getSelection()); + }, + "Ctrl-Y": function(cm) { + var start = cm.getCursor(); + cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste"); + cm.setSelection(start, cm.getCursor()); + }, + "Alt-Y": function(cm) {cm.replaceSelection(popFromRing());}, + + "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark, + + "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1), + "Right": move(byChar, 1), "Left": move(byChar, -1), + "Ctrl-D": function(cm) { killTo(cm, byChar, 1); }, + "Delete": function(cm) { killTo(cm, byChar, 1); }, + "Ctrl-H": function(cm) { killTo(cm, byChar, -1); }, + "Backspace": function(cm) { killTo(cm, byChar, -1); }, + + "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1), + "Alt-D": function(cm) { killTo(cm, byWord, 1); }, + "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); }, + + "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1), + "Down": move(byLine, 1), "Up": move(byLine, -1), + "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "End": "goLineEnd", "Home": "goLineStart", + + "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1), + "PageUp": move(byPage, -1), "PageDown": move(byPage, 1), + + "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1), + + "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1), + "Alt-K": function(cm) { killTo(cm, bySentence, 1); }, + + "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); }, + "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); }, + "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1), + + "Shift-Ctrl-Alt-2": function(cm) { + cm.setSelection(findEnd(cm, byExpr, 1), cm.getCursor()); + }, + "Ctrl-Alt-T": function(cm) { + var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1); + var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1); + cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) + + cm.getRange(leftStart, leftEnd), leftStart, rightEnd); + }, + "Ctrl-Alt-U": repeated(toEnclosingExpr), + + "Alt-Space": function(cm) { + var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line); + while (from && /\s/.test(text.charAt(from - 1))) --from; + while (to < text.length && /\s/.test(text.charAt(to))) ++to; + cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to)); + }, + "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }), + "Ctrl-T": repeated(function(cm) { + var pos = cm.getCursor(); + if (pos.ch < cm.getLine(pos.line).length) pos = Pos(pos.line, pos.ch + 1); + var from = cm.findPosH(pos, -2, "char"); + var range = cm.getRange(from, pos); + if (range.length != 2) return; + cm.setSelection(from, pos); + cm.replaceSelection(range.charAt(1) + range.charAt(0), "end"); + }), + + "Alt-C": repeated(function(cm) { + operateOnWord(cm, function(w) { + var letter = w.search(/\w/); + if (letter == -1) return w; + return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase(); + }); + }), + "Alt-U": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toUpperCase(); }); + }), + "Alt-L": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toLowerCase(); }); + }), + + "Alt-;": "toggleComment", + + "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"), + "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"), + "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", + "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace", + "Alt-/": "autocomplete", + "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto", + + "Alt-G": function(cm) {cm.setOption("keyMap", "emacs-Alt-G");}, + "Ctrl-X": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-X");}, + "Ctrl-Q": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-Q");}, + "Ctrl-U": addPrefixMap + }; + + CodeMirror.keyMap["emacs-Ctrl-X"] = { + "Tab": function(cm) { + cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit")); + }, + "Ctrl-X": function(cm) { + cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor")); + }, + + "Ctrl-S": "save", "Ctrl-W": "save", "S": "saveAll", "F": "open", "U": repeated("undo"), "K": "close", + "Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); }, + auto: "emacs", nofallthrough: true, disableInput: true + }; + + CodeMirror.keyMap["emacs-Alt-G"] = { + "G": function(cm) { + var prefix = getPrefix(cm, true); + if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1); + + getInput(cm, "Goto line", function(str) { + var num; + if (str && !isNaN(num = Number(str)) && num == num|0 && num > 0) + cm.setCursor(num - 1); + }); + }, + auto: "emacs", nofallthrough: true, disableInput: true + }; + + CodeMirror.keyMap["emacs-Ctrl-Q"] = { + "Tab": repeated("insertTab"), + auto: "emacs", nofallthrough: true + }; + + var prefixMap = {"Ctrl-G": clearPrefix}; + function regPrefix(d) { + prefixMap[d] = function(cm) { addPrefix(cm, d); }; + keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); }; + prefixPreservingKeys["Ctrl-" + d] = true; + } + for (var i = 0; i < 10; ++i) regPrefix(String(i)); + regPrefix("-"); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/extra.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/extra.js new file mode 100644 index 000000000..18dd5a979 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/extra.js @@ -0,0 +1,43 @@ +// A number of additional default bindings that are too obscure to +// include in the core codemirror.js file. + +(function() { + "use strict"; + + var Pos = CodeMirror.Pos; + + function moveLines(cm, start, end, dist) { + if (!dist || start > end) return 0; + + var from = cm.clipPos(Pos(start, 0)), to = cm.clipPos(Pos(end)); + var text = cm.getRange(from, to); + + if (start <= cm.firstLine()) + cm.replaceRange("", from, Pos(to.line + 1, 0)); + else + cm.replaceRange("", Pos(from.line - 1), to); + var target = from.line + dist; + if (target <= cm.firstLine()) { + cm.replaceRange(text + "\n", Pos(target, 0)); + return cm.firstLine() - from.line; + } else { + var targetPos = cm.clipPos(Pos(target - 1)); + cm.replaceRange("\n" + text, targetPos); + return targetPos.line + 1 - from.line; + } + } + + function moveSelectedLines(cm, dist) { + var head = cm.getCursor("head"), anchor = cm.getCursor("anchor"); + cm.operation(function() { + var moved = moveLines(cm, Math.min(head.line, anchor.line), Math.max(head.line, anchor.line), dist); + cm.setSelection(Pos(anchor.line + moved, anchor.ch), Pos(head.line + moved, head.ch)); + }); + } + + CodeMirror.commands.moveLinesUp = function(cm) { moveSelectedLines(cm, -1); }; + CodeMirror.commands.moveLinesDown = function(cm) { moveSelectedLines(cm, 1); }; + + CodeMirror.keyMap["default"]["Alt-Up"] = "moveLinesUp"; + CodeMirror.keyMap["default"]["Alt-Down"] = "moveLinesDown"; +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/vim.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/vim.js new file mode 100644 index 000000000..e67a46ed7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/keymap/vim.js @@ -0,0 +1,3704 @@ +/** + * Supported keybindings: + * + * Motion: + * h, j, k, l + * gj, gk + * e, E, w, W, b, B, ge, gE + * f, F, t, T + * $, ^, 0, -, +, _ + * gg, G + * % + * ', ` + * + * Operator: + * d, y, c + * dd, yy, cc + * g~, g~g~ + * >, <, >>, << + * + * Operator-Motion: + * x, X, D, Y, C, ~ + * + * Action: + * a, i, s, A, I, S, o, O + * zz, z., z, zt, zb, z- + * J + * u, Ctrl-r + * m + * r + * + * Modes: + * ESC - leave insert mode, visual mode, and clear input state. + * Ctrl-[, Ctrl-c - same as ESC. + * + * Registers: unamed, -, a-z, A-Z, 0-9 + * (Does not respect the special case for number registers when delete + * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) + * TODO: Implement the remaining registers. + * Marks: a-z, A-Z, and 0-9 + * TODO: Implement the remaining special marks. They have more complex + * behavior. + * + * Events: + * 'vim-mode-change' - raised on the editor anytime the current mode changes, + * Event object: {mode: "visual", subMode: "linewise"} + * + * Code structure: + * 1. Default keymap + * 2. Variable declarations and short basic helpers + * 3. Instance (External API) implementation + * 4. Internal state tracking objects (input state, counter) implementation + * and instanstiation + * 5. Key handler (the main command dispatcher) implementation + * 6. Motion, operator, and action implementations + * 7. Helper functions for the key handler, motions, operators, and actions + * 8. Set up Vim to work as a keymap for CodeMirror. + */ + +(function() { + 'use strict'; + + var defaultKeymap = [ + // Key to key mapping. This goes first to make it possible to override + // existing mappings. + { keys: [''], type: 'keyToKey', toKeys: ['h'] }, + { keys: [''], type: 'keyToKey', toKeys: ['l'] }, + { keys: [''], type: 'keyToKey', toKeys: ['k'] }, + { keys: [''], type: 'keyToKey', toKeys: ['j'] }, + { keys: [''], type: 'keyToKey', toKeys: ['l'] }, + { keys: [''], type: 'keyToKey', toKeys: ['h'] }, + { keys: [''], type: 'keyToKey', toKeys: ['W'] }, + { keys: [''], type: 'keyToKey', toKeys: ['B'] }, + { keys: [''], type: 'keyToKey', toKeys: ['w'] }, + { keys: [''], type: 'keyToKey', toKeys: ['b'] }, + { keys: [''], type: 'keyToKey', toKeys: ['j'] }, + { keys: [''], type: 'keyToKey', toKeys: ['k'] }, + { keys: ['C-['], type: 'keyToKey', toKeys: [''] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, + { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, + { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' }, + { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' }, + { keys: [''], type: 'keyToKey', toKeys: ['0'] }, + { keys: [''], type: 'keyToKey', toKeys: ['$'] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + // Motions + { keys: ['H'], type: 'motion', + motion: 'moveToTopLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['M'], type: 'motion', + motion: 'moveToMiddleLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['L'], type: 'motion', + motion: 'moveToBottomLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['h'], type: 'motion', + motion: 'moveByCharacters', + motionArgs: { forward: false }}, + { keys: ['l'], type: 'motion', + motion: 'moveByCharacters', + motionArgs: { forward: true }}, + { keys: ['j'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, linewise: true }}, + { keys: ['k'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: false, linewise: true }}, + { keys: ['g','j'], type: 'motion', + motion: 'moveByDisplayLines', + motionArgs: { forward: true }}, + { keys: ['g','k'], type: 'motion', + motion: 'moveByDisplayLines', + motionArgs: { forward: false }}, + { keys: ['w'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: false }}, + { keys: ['W'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: false, bigWord: true }}, + { keys: ['e'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: true, inclusive: true }}, + { keys: ['E'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: true, bigWord: true, + inclusive: true }}, + { keys: ['b'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: false }}, + { keys: ['B'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: false, bigWord: true }}, + { keys: ['g', 'e'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: true, inclusive: true }}, + { keys: ['g', 'E'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: true, bigWord: true, + inclusive: true }}, + { keys: ['{'], type: 'motion', motion: 'moveByParagraph', + motionArgs: { forward: false, toJumplist: true }}, + { keys: ['}'], type: 'motion', motion: 'moveByParagraph', + motionArgs: { forward: true, toJumplist: true }}, + { keys: [''], type: 'motion', + motion: 'moveByPage', motionArgs: { forward: true }}, + { keys: [''], type: 'motion', + motion: 'moveByPage', motionArgs: { forward: false }}, + { keys: [''], type: 'motion', + motion: 'moveByScroll', + motionArgs: { forward: true, explicitRepeat: true }}, + { keys: [''], type: 'motion', + motion: 'moveByScroll', + motionArgs: { forward: false, explicitRepeat: true }}, + { keys: ['g', 'g'], type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: ['G'], type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' }, + { keys: ['^'], type: 'motion', + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['+'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, toFirstChar:true }}, + { keys: ['-'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: false, toFirstChar:true }}, + { keys: ['_'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, + { keys: ['$'], type: 'motion', + motion: 'moveToEol', + motionArgs: { inclusive: true }}, + { keys: ['%'], type: 'motion', + motion: 'moveToMatchedSymbol', + motionArgs: { inclusive: true, toJumplist: true }}, + { keys: ['f', 'character'], type: 'motion', + motion: 'moveToCharacter', + motionArgs: { forward: true , inclusive: true }}, + { keys: ['F', 'character'], type: 'motion', + motion: 'moveToCharacter', + motionArgs: { forward: false }}, + { keys: ['t', 'character'], type: 'motion', + motion: 'moveTillCharacter', + motionArgs: { forward: true, inclusive: true }}, + { keys: ['T', 'character'], type: 'motion', + motion: 'moveTillCharacter', + motionArgs: { forward: false }}, + { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch', + motionArgs: { forward: true }}, + { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch', + motionArgs: { forward: false }}, + { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark', + motionArgs: {toJumplist: true}}, + { keys: ['`', 'character'], type: 'motion', motion: 'goToMark', + motionArgs: {toJumplist: true}}, + { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, + { keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, + { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, + { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, + { keys: [']', 'character'], type: 'motion', + motion: 'moveToSymbol', + motionArgs: { forward: true, toJumplist: true}}, + { keys: ['[', 'character'], type: 'motion', + motion: 'moveToSymbol', + motionArgs: { forward: false, toJumplist: true}}, + { keys: ['|'], type: 'motion', + motion: 'moveToColumn', + motionArgs: { }}, + // Operators + { keys: ['d'], type: 'operator', operator: 'delete' }, + { keys: ['y'], type: 'operator', operator: 'yank' }, + { keys: ['c'], type: 'operator', operator: 'change' }, + { keys: ['>'], type: 'operator', operator: 'indent', + operatorArgs: { indentRight: true }}, + { keys: ['<'], type: 'operator', operator: 'indent', + operatorArgs: { indentRight: false }}, + { keys: ['g', '~'], type: 'operator', operator: 'swapcase' }, + { keys: ['n'], type: 'motion', motion: 'findNext', + motionArgs: { forward: true, toJumplist: true }}, + { keys: ['N'], type: 'motion', motion: 'findNext', + motionArgs: { forward: false, toJumplist: true }}, + // Operator-Motion dual commands + { keys: ['x'], type: 'operatorMotion', operator: 'delete', + motion: 'moveByCharacters', motionArgs: { forward: true }, + operatorMotionArgs: { visualLine: false }}, + { keys: ['X'], type: 'operatorMotion', operator: 'delete', + motion: 'moveByCharacters', motionArgs: { forward: false }, + operatorMotionArgs: { visualLine: true }}, + { keys: ['D'], type: 'operatorMotion', operator: 'delete', + motion: 'moveToEol', motionArgs: { inclusive: true }, + operatorMotionArgs: { visualLine: true }}, + { keys: ['Y'], type: 'operatorMotion', operator: 'yank', + motion: 'moveToEol', motionArgs: { inclusive: true }, + operatorMotionArgs: { visualLine: true }}, + { keys: ['C'], type: 'operatorMotion', + operator: 'change', + motion: 'moveToEol', motionArgs: { inclusive: true }, + operatorMotionArgs: { visualLine: true }}, + { keys: ['~'], type: 'operatorMotion', + operator: 'swapcase', operatorArgs: { shouldMoveCursor: true }, + motion: 'moveByCharacters', motionArgs: { forward: true }}, + // Actions + { keys: [''], type: 'action', action: 'jumpListWalk', + actionArgs: { forward: true }}, + { keys: [''], type: 'action', action: 'jumpListWalk', + actionArgs: { forward: false }}, + { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'charAfter' }}, + { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'eol' }}, + { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'inplace' }}, + { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'firstNonBlank' }}, + { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', + isEdit: true, interlaceInsertRepeat: true, + actionArgs: { after: true }}, + { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', + isEdit: true, interlaceInsertRepeat: true, + actionArgs: { after: false }}, + { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, + { keys: ['V'], type: 'action', action: 'toggleVisualMode', + actionArgs: { linewise: true }}, + { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true }, + { keys: ['p'], type: 'action', action: 'paste', isEdit: true, + actionArgs: { after: true, isEdit: true }}, + { keys: ['P'], type: 'action', action: 'paste', isEdit: true, + actionArgs: { after: false, isEdit: true }}, + { keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true }, + { keys: ['@', 'character'], type: 'action', action: 'replayMacro' }, + { keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' }, + // Handle Replace-mode as a special case of insert mode. + { keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { replace: true }}, + { keys: ['u'], type: 'action', action: 'undo' }, + { keys: [''], type: 'action', action: 'redo' }, + { keys: ['m', 'character'], type: 'action', action: 'setMark' }, + { keys: ['"', 'character'], type: 'action', action: 'setRegister' }, + { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'center' }}, + { keys: ['z', '.'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'center' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['z', 't'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'top' }}, + { keys: ['z', ''], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'top' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['z', '-'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'bottom' }}, + { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'bottom' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['.'], type: 'action', action: 'repeatLastEdit' }, + { keys: [''], type: 'action', action: 'incrementNumberToken', + isEdit: true, + actionArgs: {increase: true, backtrack: false}}, + { keys: [''], type: 'action', action: 'incrementNumberToken', + isEdit: true, + actionArgs: {increase: false, backtrack: false}}, + // Text object motions + { keys: ['a', 'character'], type: 'motion', + motion: 'textObjectManipulation' }, + { keys: ['i', 'character'], type: 'motion', + motion: 'textObjectManipulation', + motionArgs: { textObjectInner: true }}, + // Search + { keys: ['/'], type: 'search', + searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, + { keys: ['?'], type: 'search', + searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, + { keys: ['*'], type: 'search', + searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, + { keys: ['#'], type: 'search', + searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, + // Ex command + { keys: [':'], type: 'ex' } + ]; + + var Vim = function() { + CodeMirror.defineOption('vimMode', false, function(cm, val) { + if (val) { + cm.setOption('keyMap', 'vim'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + cm.on('beforeSelectionChange', beforeSelectionChange); + maybeInitVimState(cm); + } else if (cm.state.vim) { + cm.setOption('keyMap', 'default'); + cm.off('beforeSelectionChange', beforeSelectionChange); + cm.state.vim = null; + } + }); + function beforeSelectionChange(cm, cur) { + var vim = cm.state.vim; + if (vim.insertMode || vim.exMode) return; + + var head = cur.head; + if (head.ch && head.ch == cm.doc.getLine(head.line).length) { + head.ch--; + } + } + + var numberRegex = /[\d]/; + var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)]; + function makeKeyRange(start, size) { + var keys = []; + for (var i = start; i < start + size; i++) { + keys.push(String.fromCharCode(i)); + } + return keys; + } + var upperCaseAlphabet = makeKeyRange(65, 26); + var lowerCaseAlphabet = makeKeyRange(97, 26); + var numbers = makeKeyRange(48, 10); + var specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split(''); + var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', + 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; + var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); + var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']); + + function isLine(cm, line) { + return line >= cm.firstLine() && line <= cm.lastLine(); + } + function isLowerCase(k) { + return (/^[a-z]$/).test(k); + } + function isMatchableSymbol(k) { + return '()[]{}'.indexOf(k) != -1; + } + function isNumber(k) { + return numberRegex.test(k); + } + function isUpperCase(k) { + return (/^[A-Z]$/).test(k); + } + function isWhiteSpaceString(k) { + return (/^\s*$/).test(k); + } + function inArray(val, arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == val) { + return true; + } + } + return false; + } + + var createCircularJumpList = function() { + var size = 100; + var pointer = -1; + var head = 0; + var tail = 0; + var buffer = new Array(size); + function add(cm, oldCur, newCur) { + var current = pointer % size; + var curMark = buffer[current]; + function useNextSlot(cursor) { + var next = ++pointer % size; + var trashMark = buffer[next]; + if (trashMark) { + trashMark.clear(); + } + buffer[next] = cm.setBookmark(cursor); + } + if (curMark) { + var markPos = curMark.find(); + // avoid recording redundant cursor position + if (markPos && !cursorEqual(markPos, oldCur)) { + useNextSlot(oldCur); + } + } else { + useNextSlot(oldCur); + } + useNextSlot(newCur); + head = pointer; + tail = pointer - size + 1; + if (tail < 0) { + tail = 0; + } + } + function move(cm, offset) { + pointer += offset; + if (pointer > head) { + pointer = head; + } else if (pointer < tail) { + pointer = tail; + } + var mark = buffer[(size + pointer) % size]; + // skip marks that are temporarily removed from text buffer + if (mark && !mark.find()) { + var inc = offset > 0 ? 1 : -1; + var newCur; + var oldCur = cm.getCursor(); + do { + pointer += inc; + mark = buffer[(size + pointer) % size]; + // skip marks that are the same as current position + if (mark && + (newCur = mark.find()) && + !cursorEqual(oldCur, newCur)) { + break; + } + } while (pointer < head && pointer > tail); + } + return mark; + } + return { + cachedCursor: undefined, //used for # and * jumps + add: add, + move: move + }; + }; + + var createMacroState = function() { + return { + macroKeyBuffer: [], + latestRegister: undefined, + inReplay: false, + lastInsertModeChanges: { + changes: [], // Change list + expectCursorActivityForChange: false // Set to true on change, false on cursorActivity. + }, + enteredMacroMode: undefined, + isMacroPlaying: false, + toggle: function(cm, registerName) { + if (this.enteredMacroMode) { //onExit + this.enteredMacroMode(); // close dialog + this.enteredMacroMode = undefined; + } else { //onEnter + this.latestRegister = registerName; + this.enteredMacroMode = cm.openDialog( + '(recording)['+registerName+']', null, {bottom:true}); + } + } + }; + }; + + + function maybeInitVimState(cm) { + if (!cm.state.vim) { + // Store instance state in the CodeMirror object. + cm.state.vim = { + inputState: new InputState(), + // Vim's input state that triggered the last edit, used to repeat + // motions and operators with '.'. + lastEditInputState: undefined, + // Vim's action command before the last edit, used to repeat actions + // with '.' and insert mode repeat. + lastEditActionCommand: undefined, + // When using jk for navigation, if you move from a longer line to a + // shorter line, the cursor may clip to the end of the shorter line. + // If j is pressed again and cursor goes to the next line, the + // cursor should go back to its horizontal position on the longer + // line if it can. This is to keep track of the horizontal position. + lastHPos: -1, + // Doing the same with screen-position for gj/gk + lastHSPos: -1, + // The last motion command run. Cleared if a non-motion command gets + // executed in between. + lastMotion: null, + marks: {}, + insertMode: false, + // Repeat count for changes made in insert mode, triggered by key + // sequences like 3,i. Only exists when insertMode is true. + insertModeRepeat: undefined, + visualMode: false, + // If we are in visual line mode. No effect if visualMode is false. + visualLine: false + }; + } + return cm.state.vim; + } + var vimGlobalState; + function resetVimGlobalState() { + vimGlobalState = { + // The current search query. + searchQuery: null, + // Whether we are searching backwards. + searchIsReversed: false, + jumpList: createCircularJumpList(), + macroModeState: createMacroState(), + // Recording latest f, t, F or T motion command. + lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, + registerController: new RegisterController({}) + }; + } + + var vimApi= { + buildKeyMap: function() { + // TODO: Convert keymap into dictionary format for fast lookup. + }, + // Testing hook, though it might be useful to expose the register + // controller anyways. + getRegisterController: function() { + return vimGlobalState.registerController; + }, + // Testing hook. + resetVimGlobalState_: resetVimGlobalState, + + // Testing hook. + getVimGlobalState_: function() { + return vimGlobalState; + }, + + // Testing hook. + maybeInitVimState_: maybeInitVimState, + + InsertModeKey: InsertModeKey, + map: function(lhs, rhs) { + // Add user defined key bindings. + exCommandDispatcher.map(lhs, rhs); + }, + defineEx: function(name, prefix, func){ + if (name.indexOf(prefix) !== 0) { + throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); + } + exCommands[name]=func; + exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; + }, + // This is the outermost function called by CodeMirror, after keys have + // been mapped to their Vim equivalents. + handleKey: function(cm, key) { + var command; + var vim = maybeInitVimState(cm); + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.enteredMacroMode) { + if (key == 'q') { + actions.exitMacroRecordMode(); + vim.inputState = new InputState(); + return; + } + } + if (key == '') { + // Clear input state and get back to normal mode. + vim.inputState = new InputState(); + if (vim.visualMode) { + exitVisualMode(cm); + } + return; + } + // Enter visual mode when the mouse selects text. + if (!vim.visualMode && + !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { + vim.visualMode = true; + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); + cm.on('mousedown', exitVisualMode); + } + if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) { + // Have to special case 0 since it's both a motion and a number. + command = commandDispatcher.matchCommand(key, defaultKeymap, vim); + } + if (!command) { + if (isNumber(key)) { + // Increment count unless count is 0 and key is 0. + vim.inputState.pushRepeatDigit(key); + } + return; + } + if (command.type == 'keyToKey') { + // TODO: prevent infinite recursion. + for (var i = 0; i < command.toKeys.length; i++) { + this.handleKey(cm, command.toKeys[i]); + } + } else { + if (macroModeState.enteredMacroMode) { + logKey(macroModeState, key); + } + commandDispatcher.processCommand(cm, vim, command); + } + }, + handleEx: function(cm, input) { + exCommandDispatcher.processCommand(cm, input); + } + }; + + // Represents the current input state. + function InputState() { + this.prefixRepeat = []; + this.motionRepeat = []; + + this.operator = null; + this.operatorArgs = null; + this.motion = null; + this.motionArgs = null; + this.keyBuffer = []; // For matching multi-key commands. + this.registerName = null; // Defaults to the unamed register. + } + InputState.prototype.pushRepeatDigit = function(n) { + if (!this.operator) { + this.prefixRepeat = this.prefixRepeat.concat(n); + } else { + this.motionRepeat = this.motionRepeat.concat(n); + } + }; + InputState.prototype.getRepeat = function() { + var repeat = 0; + if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { + repeat = 1; + if (this.prefixRepeat.length > 0) { + repeat *= parseInt(this.prefixRepeat.join(''), 10); + } + if (this.motionRepeat.length > 0) { + repeat *= parseInt(this.motionRepeat.join(''), 10); + } + } + return repeat; + }; + + /* + * Register stores information about copy and paste registers. Besides + * text, a register must store whether it is linewise (i.e., when it is + * pasted, should it insert itself into a new line, or should the text be + * inserted at the cursor position.) + */ + function Register(text, linewise) { + this.clear(); + if (text) { + this.set(text, linewise); + } + } + Register.prototype = { + set: function(text, linewise) { + this.text = text; + this.linewise = !!linewise; + }, + append: function(text, linewise) { + // if this register has ever been set to linewise, use linewise. + if (linewise || this.linewise) { + this.text += '\n' + text; + this.linewise = true; + } else { + this.text += text; + } + }, + clear: function() { + this.text = ''; + this.linewise = false; + }, + toString: function() { return this.text; } + }; + + /* + * vim registers allow you to keep many independent copy and paste buffers. + * See http://usevim.com/2012/04/13/registers/ for an introduction. + * + * RegisterController keeps the state of all the registers. An initial + * state may be passed in. The unnamed register '"' will always be + * overridden. + */ + function RegisterController(registers) { + this.registers = registers; + this.unamedRegister = registers['"'] = new Register(); + } + RegisterController.prototype = { + pushText: function(registerName, operator, text, linewise) { + if (linewise && text.charAt(0) == '\n') { + text = text.slice(1) + '\n'; + } + if(linewise && text.charAt(text.length - 1) !== '\n'){ + text += '\n'; + } + // Lowercase and uppercase registers refer to the same register. + // Uppercase just means append. + var register = this.isValidRegister(registerName) ? + this.getRegister(registerName) : null; + // if no register/an invalid register was specified, things go to the + // default registers + if (!register) { + switch (operator) { + case 'yank': + // The 0 register contains the text from the most recent yank. + this.registers['0'] = new Register(text, linewise); + break; + case 'delete': + case 'change': + if (text.indexOf('\n') == -1) { + // Delete less than 1 line. Update the small delete register. + this.registers['-'] = new Register(text, linewise); + } else { + // Shift down the contents of the numbered registers and put the + // deleted text into register 1. + this.shiftNumericRegisters_(); + this.registers['1'] = new Register(text, linewise); + } + break; + } + // Make sure the unnamed register is set to what just happened + this.unamedRegister.set(text, linewise); + return; + } + + // If we've gotten to this point, we've actually specified a register + var append = isUpperCase(registerName); + if (append) { + register.append(text, linewise); + // The unamed register always has the same value as the last used + // register. + this.unamedRegister.append(text, linewise); + } else { + register.set(text, linewise); + this.unamedRegister.set(text, linewise); + } + }, + setRegisterText: function(name, text, linewise) { + this.getRegister(name).set(text, linewise); + }, + // Gets the register named @name. If one of @name doesn't already exist, + // create it. If @name is invalid, return the unamedRegister. + getRegister: function(name) { + if (!this.isValidRegister(name)) { + return this.unamedRegister; + } + name = name.toLowerCase(); + if (!this.registers[name]) { + this.registers[name] = new Register(); + } + return this.registers[name]; + }, + isValidRegister: function(name) { + return name && inArray(name, validRegisters); + }, + shiftNumericRegisters_: function() { + for (var i = 9; i >= 2; i--) { + this.registers[i] = this.getRegister('' + (i - 1)); + } + } + }; + + var commandDispatcher = { + matchCommand: function(key, keyMap, vim) { + var inputState = vim.inputState; + var keys = inputState.keyBuffer.concat(key); + var matchedCommands = []; + var selectedCharacter; + for (var i = 0; i < keyMap.length; i++) { + var command = keyMap[i]; + if (matchKeysPartial(keys, command.keys)) { + if (inputState.operator && command.type == 'action') { + // Ignore matched action commands after an operator. Operators + // only operate on motions. This check is really for text + // objects since aW, a[ etcs conflicts with a. + continue; + } + // Match commands that take as an argument. + if (command.keys[keys.length - 1] == 'character') { + selectedCharacter = keys[keys.length - 1]; + if(selectedCharacter.length>1){ + switch(selectedCharacter){ + case '': + selectedCharacter='\n'; + break; + case '': + selectedCharacter=' '; + break; + default: + continue; + } + } + } + // Add the command to the list of matched commands. Choose the best + // command later. + matchedCommands.push(command); + } + } + + // Returns the command if it is a full match, or null if not. + function getFullyMatchedCommandOrNull(command) { + if (keys.length < command.keys.length) { + // Matches part of a multi-key command. Buffer and wait for next + // stroke. + inputState.keyBuffer.push(key); + return null; + } else { + if (command.keys[keys.length - 1] == 'character') { + inputState.selectedCharacter = selectedCharacter; + } + // Clear the buffer since a full match was found. + inputState.keyBuffer = []; + return command; + } + } + + if (!matchedCommands.length) { + // Clear the buffer since there were no matches. + inputState.keyBuffer = []; + return null; + } else if (matchedCommands.length == 1) { + return getFullyMatchedCommandOrNull(matchedCommands[0]); + } else { + // Find the best match in the list of matchedCommands. + var context = vim.visualMode ? 'visual' : 'normal'; + var bestMatch = matchedCommands[0]; // Default to first in the list. + for (var i = 0; i < matchedCommands.length; i++) { + if (matchedCommands[i].context == context) { + bestMatch = matchedCommands[i]; + break; + } + } + return getFullyMatchedCommandOrNull(bestMatch); + } + }, + processCommand: function(cm, vim, command) { + vim.inputState.repeatOverride = command.repeatOverride; + switch (command.type) { + case 'motion': + this.processMotion(cm, vim, command); + break; + case 'operator': + this.processOperator(cm, vim, command); + break; + case 'operatorMotion': + this.processOperatorMotion(cm, vim, command); + break; + case 'action': + this.processAction(cm, vim, command); + break; + case 'search': + this.processSearch(cm, vim, command); + break; + case 'ex': + case 'keyToEx': + this.processEx(cm, vim, command); + break; + default: + break; + } + }, + processMotion: function(cm, vim, command) { + vim.inputState.motion = command.motion; + vim.inputState.motionArgs = copyArgs(command.motionArgs); + this.evalInput(cm, vim); + }, + processOperator: function(cm, vim, command) { + var inputState = vim.inputState; + if (inputState.operator) { + if (inputState.operator == command.operator) { + // Typing an operator twice like 'dd' makes the operator operate + // linewise + inputState.motion = 'expandToLine'; + inputState.motionArgs = { linewise: true }; + this.evalInput(cm, vim); + return; + } else { + // 2 different operators in a row doesn't make sense. + vim.inputState = new InputState(); + } + } + inputState.operator = command.operator; + inputState.operatorArgs = copyArgs(command.operatorArgs); + if (vim.visualMode) { + // Operating on a selection in visual mode. We don't need a motion. + this.evalInput(cm, vim); + } + }, + processOperatorMotion: function(cm, vim, command) { + var visualMode = vim.visualMode; + var operatorMotionArgs = copyArgs(command.operatorMotionArgs); + if (operatorMotionArgs) { + // Operator motions may have special behavior in visual mode. + if (visualMode && operatorMotionArgs.visualLine) { + vim.visualLine = true; + } + } + this.processOperator(cm, vim, command); + if (!visualMode) { + this.processMotion(cm, vim, command); + } + }, + processAction: function(cm, vim, command) { + var inputState = vim.inputState; + var repeat = inputState.getRepeat(); + var repeatIsExplicit = !!repeat; + var actionArgs = copyArgs(command.actionArgs) || {}; + if (inputState.selectedCharacter) { + actionArgs.selectedCharacter = inputState.selectedCharacter; + } + // Actions may or may not have motions and operators. Do these first. + if (command.operator) { + this.processOperator(cm, vim, command); + } + if (command.motion) { + this.processMotion(cm, vim, command); + } + if (command.motion || command.operator) { + this.evalInput(cm, vim); + } + actionArgs.repeat = repeat || 1; + actionArgs.repeatIsExplicit = repeatIsExplicit; + actionArgs.registerName = inputState.registerName; + vim.inputState = new InputState(); + vim.lastMotion = null; + if (command.isEdit) { + this.recordLastEdit(vim, inputState, command); + } + actions[command.action](cm, actionArgs, vim); + }, + processSearch: function(cm, vim, command) { + if (!cm.getSearchCursor) { + // Search depends on SearchCursor. + return; + } + var forward = command.searchArgs.forward; + getSearchState(cm).setReversed(!forward); + var promptPrefix = (forward) ? '/' : '?'; + var originalQuery = getSearchState(cm).getQuery(); + var originalScrollPos = cm.getScrollInfo(); + function handleQuery(query, ignoreCase, smartCase) { + try { + updateSearchQuery(cm, query, ignoreCase, smartCase); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + query); + return; + } + commandDispatcher.processMotion(cm, vim, { + type: 'motion', + motion: 'findNext', + motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } + }); + } + function onPromptClose(query) { + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + handleQuery(query, true /** ignoreCase */, true /** smartCase */); + } + function onPromptKeyUp(_e, query) { + var parsedQuery; + try { + parsedQuery = updateSearchQuery(cm, query, + true /** ignoreCase */, true /** smartCase */); + } catch (e) { + // Swallow bad regexes for incremental search. + } + if (parsedQuery) { + cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); + } else { + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + } + } + function onPromptKeyDown(e, _query, close) { + var keyName = CodeMirror.keyName(e); + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { + updateSearchQuery(cm, originalQuery); + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + + CodeMirror.e_stop(e); + close(); + cm.focus(); + } + } + switch (command.searchArgs.querySrc) { + case 'prompt': + showPrompt(cm, { + onClose: onPromptClose, + prefix: promptPrefix, + desc: searchPromptDesc, + onKeyUp: onPromptKeyUp, + onKeyDown: onPromptKeyDown + }); + break; + case 'wordUnderCursor': + var word = expandWordUnderCursor(cm, false /** inclusive */, + true /** forward */, false /** bigWord */, + true /** noSymbol */); + var isKeyword = true; + if (!word) { + word = expandWordUnderCursor(cm, false /** inclusive */, + true /** forward */, false /** bigWord */, + false /** noSymbol */); + isKeyword = false; + } + if (!word) { + return; + } + var query = cm.getLine(word.start.line).substring(word.start.ch, + word.end.ch); + if (isKeyword) { + query = '\\b' + query + '\\b'; + } else { + query = escapeRegex(query); + } + + // cachedCursor is used to save the old position of the cursor + // when * or # causes vim to seek for the nearest word and shift + // the cursor before entering the motion. + vimGlobalState.jumpList.cachedCursor = cm.getCursor(); + cm.setCursor(word.start); + + handleQuery(query, true /** ignoreCase */, false /** smartCase */); + break; + } + }, + processEx: function(cm, vim, command) { + function onPromptClose(input) { + // Give the prompt some time to close so that if processCommand shows + // an error, the elements don't overlap. + exCommandDispatcher.processCommand(cm, input); + } + function onPromptKeyDown(e, _input, close) { + var keyName = CodeMirror.keyName(e); + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { + CodeMirror.e_stop(e); + close(); + cm.focus(); + } + } + if (command.type == 'keyToEx') { + // Handle user defined Ex to Ex mappings + exCommandDispatcher.processCommand(cm, command.exArgs.input); + } else { + if (vim.visualMode) { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', + onKeyDown: onPromptKeyDown}); + } else { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', + onKeyDown: onPromptKeyDown}); + } + } + }, + evalInput: function(cm, vim) { + // If the motion comand is set, execute both the operator and motion. + // Otherwise return. + var inputState = vim.inputState; + var motion = inputState.motion; + var motionArgs = inputState.motionArgs || {}; + var operator = inputState.operator; + var operatorArgs = inputState.operatorArgs || {}; + var registerName = inputState.registerName; + var selectionEnd = cm.getCursor('head'); + var selectionStart = cm.getCursor('anchor'); + // The difference between cur and selection cursors are that cur is + // being operated on and ignores that there is a selection. + var curStart = copyCursor(selectionEnd); + var curOriginal = copyCursor(curStart); + var curEnd; + var repeat; + if (operator) { + this.recordLastEdit(vim, inputState); + } + if (inputState.repeatOverride !== undefined) { + // If repeatOverride is specified, that takes precedence over the + // input state's repeat. Used by Ex mode and can be user defined. + repeat = inputState.repeatOverride; + } else { + repeat = inputState.getRepeat(); + } + if (repeat > 0 && motionArgs.explicitRepeat) { + motionArgs.repeatIsExplicit = true; + } else if (motionArgs.noRepeat || + (!motionArgs.explicitRepeat && repeat === 0)) { + repeat = 1; + motionArgs.repeatIsExplicit = false; + } + if (inputState.selectedCharacter) { + // If there is a character input, stick it in all of the arg arrays. + motionArgs.selectedCharacter = operatorArgs.selectedCharacter = + inputState.selectedCharacter; + } + motionArgs.repeat = repeat; + vim.inputState = new InputState(); + if (motion) { + var motionResult = motions[motion](cm, motionArgs, vim); + vim.lastMotion = motions[motion]; + if (!motionResult) { + return; + } + if (motionArgs.toJumplist) { + var jumpList = vimGlobalState.jumpList; + // if the current motion is # or *, use cachedCursor + var cachedCursor = jumpList.cachedCursor; + if (cachedCursor) { + recordJumpPosition(cm, cachedCursor, motionResult); + delete jumpList.cachedCursor; + } else { + recordJumpPosition(cm, curOriginal, motionResult); + } + } + if (motionResult instanceof Array) { + curStart = motionResult[0]; + curEnd = motionResult[1]; + } else { + curEnd = motionResult; + } + // TODO: Handle null returns from motion commands better. + if (!curEnd) { + curEnd = { ch: curStart.ch, line: curStart.line }; + } + if (vim.visualMode) { + // Check if the selection crossed over itself. Will need to shift + // the start point if that happened. + if (cursorIsBefore(selectionStart, selectionEnd) && + (cursorEqual(selectionStart, curEnd) || + cursorIsBefore(curEnd, selectionStart))) { + // The end of the selection has moved from after the start to + // before the start. We will shift the start right by 1. + selectionStart.ch += 1; + } else if (cursorIsBefore(selectionEnd, selectionStart) && + (cursorEqual(selectionStart, curEnd) || + cursorIsBefore(selectionStart, curEnd))) { + // The opposite happened. We will shift the start left by 1. + selectionStart.ch -= 1; + } + selectionEnd = curEnd; + if (vim.visualLine) { + if (cursorIsBefore(selectionStart, selectionEnd)) { + selectionStart.ch = 0; + + var lastLine = cm.lastLine(); + if (selectionEnd.line > lastLine) { + selectionEnd.line = lastLine; + } + selectionEnd.ch = lineLength(cm, selectionEnd.line); + } else { + selectionEnd.ch = 0; + selectionStart.ch = lineLength(cm, selectionStart.line); + } + } + cm.setSelection(selectionStart, selectionEnd); + updateMark(cm, vim, '<', + cursorIsBefore(selectionStart, selectionEnd) ? selectionStart + : selectionEnd); + updateMark(cm, vim, '>', + cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd + : selectionStart); + } else if (!operator) { + curEnd = clipCursorToContent(cm, curEnd); + cm.setCursor(curEnd.line, curEnd.ch); + } + } + + if (operator) { + var inverted = false; + vim.lastMotion = null; + operatorArgs.repeat = repeat; // Indent in visual mode needs this. + if (vim.visualMode) { + curStart = selectionStart; + curEnd = selectionEnd; + motionArgs.inclusive = true; + } + // Swap start and end if motion was backward. + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curStart; + curStart = curEnd; + curEnd = tmp; + inverted = true; + } + if (motionArgs.inclusive && !(vim.visualMode && inverted)) { + // Move the selection end one to the right to include the last + // character. + curEnd.ch++; + } + var linewise = motionArgs.linewise || + (vim.visualMode && vim.visualLine); + if (linewise) { + // Expand selection to entire line. + expandSelectionToLine(cm, curStart, curEnd); + } else if (motionArgs.forward) { + // Clip to trailing newlines only if the motion goes forward. + clipToLine(cm, curStart, curEnd); + } + operatorArgs.registerName = registerName; + // Keep track of linewise as it affects how paste and change behave. + operatorArgs.linewise = linewise; + operators[operator](cm, operatorArgs, vim, curStart, + curEnd, curOriginal); + if (vim.visualMode) { + exitVisualMode(cm); + } + } + }, + recordLastEdit: function(vim, inputState, actionCommand) { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.inReplay) { return; } + vim.lastEditInputState = inputState; + vim.lastEditActionCommand = actionCommand; + macroModeState.lastInsertModeChanges.changes = []; + macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; + } + }; + + /** + * typedef {Object{line:number,ch:number}} Cursor An object containing the + * position of the cursor. + */ + // All of the functions below return Cursor objects. + var motions = { + moveToTopLine: function(cm, motionArgs) { + var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + moveToMiddleLine: function(cm) { + var range = getUserVisibleLines(cm); + var line = Math.floor((range.top + range.bottom) * 0.5); + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + moveToBottomLine: function(cm, motionArgs) { + var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + expandToLine: function(cm, motionArgs) { + // Expands forward to end of line, and then to next line if repeat is + // >1. Does not handle backward motion! + var cur = cm.getCursor(); + return { line: cur.line + motionArgs.repeat - 1, ch: Infinity }; + }, + findNext: function(cm, motionArgs) { + var state = getSearchState(cm); + var query = state.getQuery(); + if (!query) { + return; + } + var prev = !motionArgs.forward; + // If search is initiated with ? instead of /, negate direction. + prev = (state.isReversed()) ? !prev : prev; + highlightSearchMatches(cm, query); + return findNext(cm, prev/** prev */, query, motionArgs.repeat); + }, + goToMark: function(_cm, motionArgs, vim) { + var mark = vim.marks[motionArgs.selectedCharacter]; + if (mark) { + return mark.find(); + } + return null; + }, + jumpToMark: function(cm, motionArgs, vim) { + var best = cm.getCursor(); + for (var i = 0; i < motionArgs.repeat; i++) { + var cursor = best; + for (var key in vim.marks) { + if (!isLowerCase(key)) { + continue; + } + var mark = vim.marks[key].find(); + var isWrongDirection = (motionArgs.forward) ? + cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); + + if (isWrongDirection) { + continue; + } + if (motionArgs.linewise && (mark.line == cursor.line)) { + continue; + } + + var equal = cursorEqual(cursor, best); + var between = (motionArgs.forward) ? + cusrorIsBetween(cursor, mark, best) : + cusrorIsBetween(best, mark, cursor); + + if (equal || between) { + best = mark; + } + } + } + + if (motionArgs.linewise) { + // Vim places the cursor on the first non-whitespace character of + // the line if there is one, else it places the cursor at the end + // of the line, regardless of whether a mark was found. + best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)); + } + return best; + }, + moveByCharacters: function(cm, motionArgs) { + var cur = cm.getCursor(); + var repeat = motionArgs.repeat; + var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; + return { line: cur.line, ch: ch }; + }, + moveByLines: function(cm, motionArgs, vim) { + var cur = cm.getCursor(); + var endCh = cur.ch; + // Depending what our last motion was, we may want to do different + // things. If our last motion was moving vertically, we want to + // preserve the HPos from our last horizontal move. If our last motion + // was going to the end of a line, moving vertically we should go to + // the end of the line, etc. + switch (vim.lastMotion) { + case this.moveByLines: + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveToColumn: + case this.moveToEol: + endCh = vim.lastHPos; + break; + default: + vim.lastHPos = endCh; + } + var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); + var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; + var first = cm.firstLine(); + var last = cm.lastLine(); + // Vim cancels linewise motions that start on an edge and move beyond + // that edge. It does not cancel motions that do not start on an edge. + if ((line < first && cur.line == first) || + (line > last && cur.line == last)) { + return; + } + if(motionArgs.toFirstChar){ + endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); + vim.lastHPos = endCh; + } + vim.lastHSPos = cm.charCoords({line:line, ch:endCh},'div').left; + return { line: line, ch: endCh }; + }, + moveByDisplayLines: function(cm, motionArgs, vim) { + var cur = cm.getCursor(); + switch (vim.lastMotion) { + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveByLines: + case this.moveToColumn: + case this.moveToEol: + break; + default: + vim.lastHSPos = cm.charCoords(cur,'div').left; + } + var repeat = motionArgs.repeat; + var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); + if (res.hitSide) { + if (motionArgs.forward) { + var lastCharCoords = cm.charCoords(res, 'div'); + var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; + var res = cm.coordsChar(goalCoords, 'div'); + } else { + var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div'); + resCoords.left = vim.lastHSPos; + res = cm.coordsChar(resCoords, 'div'); + } + } + vim.lastHPos = res.ch; + return res; + }, + moveByPage: function(cm, motionArgs) { + // CodeMirror only exposes functions that move the cursor page down, so + // doing this bad hack to move the cursor and move it back. evalInput + // will move the cursor to where it should be in the end. + var curStart = cm.getCursor(); + var repeat = motionArgs.repeat; + cm.moveV((motionArgs.forward ? repeat : -repeat), 'page'); + var curEnd = cm.getCursor(); + cm.setCursor(curStart); + return curEnd; + }, + moveByParagraph: function(cm, motionArgs) { + var line = cm.getCursor().line; + var repeat = motionArgs.repeat; + var inc = motionArgs.forward ? 1 : -1; + for (var i = 0; i < repeat; i++) { + if ((!motionArgs.forward && line === cm.firstLine() ) || + (motionArgs.forward && line == cm.lastLine())) { + break; + } + line += inc; + while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) { + line += inc; + } + } + return { line: line, ch: 0 }; + }, + moveByScroll: function(cm, motionArgs, vim) { + var scrollbox = cm.getScrollInfo(); + var curEnd = null; + var repeat = motionArgs.repeat; + if (!repeat) { + repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); + } + var orig = cm.charCoords(cm.getCursor(), 'local'); + motionArgs.repeat = repeat; + var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); + if (!curEnd) { + return null; + } + var dest = cm.charCoords(curEnd, 'local'); + cm.scrollTo(null, scrollbox.top + dest.top - orig.top); + return curEnd; + }, + moveByWords: function(cm, motionArgs) { + return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward, + !!motionArgs.wordEnd, !!motionArgs.bigWord); + }, + moveTillCharacter: function(cm, motionArgs) { + var repeat = motionArgs.repeat; + var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter); + var increment = motionArgs.forward ? -1 : 1; + recordLastCharacterSearch(increment, motionArgs); + if(!curEnd)return cm.getCursor(); + curEnd.ch += increment; + return curEnd; + }, + moveToCharacter: function(cm, motionArgs) { + var repeat = motionArgs.repeat; + recordLastCharacterSearch(0, motionArgs); + return moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || cm.getCursor(); + }, + moveToSymbol: function(cm, motionArgs) { + var repeat = motionArgs.repeat; + return findSymbol(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || cm.getCursor(); + }, + moveToColumn: function(cm, motionArgs, vim) { + var repeat = motionArgs.repeat; + // repeat is equivalent to which column we want to move to! + vim.lastHPos = repeat - 1; + vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left; + return moveToColumn(cm, repeat); + }, + moveToEol: function(cm, motionArgs, vim) { + var cur = cm.getCursor(); + vim.lastHPos = Infinity; + var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity }; + var end=cm.clipPos(retval); + end.ch--; + vim.lastHSPos = cm.charCoords(end,'div').left; + return retval; + }, + moveToFirstNonWhiteSpaceCharacter: function(cm) { + // Go to the start of the line where the text begins, or the end for + // whitespace-only lines + var cursor = cm.getCursor(); + return { line: cursor.line, + ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) }; + }, + moveToMatchedSymbol: function(cm) { + var cursor = cm.getCursor(); + var line = cursor.line; + var ch = cursor.ch; + var lineText = cm.getLine(line); + var symbol; + var startContext = cm.getTokenAt(cursor).type; + var startCtxLevel = getContextLevel(startContext); + do { + symbol = lineText.charAt(ch++); + if (symbol && isMatchableSymbol(symbol)) { + var endContext = cm.getTokenAt({line:line, ch:ch}).type; + var endCtxLevel = getContextLevel(endContext); + if (startCtxLevel >= endCtxLevel) { + break; + } + } + } while (symbol); + if (symbol) { + return findMatchedSymbol(cm, {line:line, ch:ch-1}, symbol); + } else { + return cursor; + } + }, + moveToStartOfLine: function(cm) { + var cursor = cm.getCursor(); + return { line: cursor.line, ch: 0 }; + }, + moveToLineOrEdgeOfDocument: function(cm, motionArgs) { + var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); + if (motionArgs.repeatIsExplicit) { + lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); + } + return { line: lineNum, + ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) }; + }, + textObjectManipulation: function(cm, motionArgs) { + var character = motionArgs.selectedCharacter; + // Inclusive is the difference between a and i + // TODO: Instead of using the additional text object map to perform text + // object operations, merge the map into the defaultKeyMap and use + // motionArgs to define behavior. Define separate entries for 'aw', + // 'iw', 'a[', 'i[', etc. + var inclusive = !motionArgs.textObjectInner; + if (!textObjects[character]) { + // No text object defined for this, don't move. + return null; + } + var tmp = textObjects[character](cm, inclusive); + var start = tmp.start; + var end = tmp.end; + return [start, end]; + }, + repeatLastCharacterSearch: function(cm, motionArgs) { + var lastSearch = vimGlobalState.lastChararacterSearch; + var repeat = motionArgs.repeat; + var forward = motionArgs.forward === lastSearch.forward; + var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); + cm.moveH(-increment, 'char'); + motionArgs.inclusive = forward ? true : false; + var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); + if (!curEnd) { + cm.moveH(increment, 'char'); + return cm.getCursor(); + } + curEnd.ch += increment; + return curEnd; + } + }; + + var operators = { + change: function(cm, operatorArgs, _vim, curStart, curEnd) { + vimGlobalState.registerController.pushText( + operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd), + operatorArgs.linewise); + if (operatorArgs.linewise) { + // Push the next line back down, if there is a next line. + var replacement = curEnd.line > cm.lastLine() ? '' : '\n'; + cm.replaceRange(replacement, curStart, curEnd); + cm.indentLine(curStart.line, 'smart'); + // null ch so setCursor moves to end of line. + curStart.ch = null; + } else { + // Exclude trailing whitespace if the range is not all whitespace. + var text = cm.getRange(curStart, curEnd); + if (!isWhiteSpaceString(text)) { + var match = (/\s+$/).exec(text); + if (match) { + curEnd = offsetCursor(curEnd, 0, - match[0].length); + } + } + cm.replaceRange('', curStart, curEnd); + } + actions.enterInsertMode(cm, {}, cm.state.vim); + cm.setCursor(curStart); + }, + // delete is a javascript keyword. + 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) { + // If the ending line is past the last line, inclusive, instead of + // including the trailing \n, include the \n before the starting line + if (operatorArgs.linewise && + curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) { + curStart.line--; + curStart.ch = lineLength(cm, curStart.line); + } + vimGlobalState.registerController.pushText( + operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd), + operatorArgs.linewise); + cm.replaceRange('', curStart, curEnd); + if (operatorArgs.linewise) { + cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + } else { + cm.setCursor(curStart); + } + }, + indent: function(cm, operatorArgs, vim, curStart, curEnd) { + var startLine = curStart.line; + var endLine = curEnd.line; + // In visual mode, n> shifts the selection right n times, instead of + // shifting n lines right once. + var repeat = (vim.visualMode) ? operatorArgs.repeat : 1; + if (operatorArgs.linewise) { + // The only way to delete a newline is to delete until the start of + // the next line, so in linewise mode evalInput will include the next + // line. We don't want this in indent, so we go back a line. + endLine--; + } + for (var i = startLine; i <= endLine; i++) { + for (var j = 0; j < repeat; j++) { + cm.indentLine(i, operatorArgs.indentRight); + } + } + cm.setCursor(curStart); + cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + }, + swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { + var toSwap = cm.getRange(curStart, curEnd); + var swapped = ''; + for (var i = 0; i < toSwap.length; i++) { + var character = toSwap.charAt(i); + swapped += isUpperCase(character) ? character.toLowerCase() : + character.toUpperCase(); + } + cm.replaceRange(swapped, curStart, curEnd); + if (!operatorArgs.shouldMoveCursor) { + cm.setCursor(curOriginal); + } + }, + yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { + vimGlobalState.registerController.pushText( + operatorArgs.registerName, 'yank', + cm.getRange(curStart, curEnd), operatorArgs.linewise); + cm.setCursor(curOriginal); + } + }; + + var actions = { + jumpListWalk: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat; + var forward = actionArgs.forward; + var jumpList = vimGlobalState.jumpList; + + var mark = jumpList.move(cm, forward ? repeat : -repeat); + var markPos = mark ? mark.find() : undefined; + markPos = markPos ? markPos : cm.getCursor(); + cm.setCursor(markPos); + }, + scrollToCursor: function(cm, actionArgs) { + var lineNum = cm.getCursor().line; + var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local'); + var height = cm.getScrollInfo().clientHeight; + var y = charCoords.top; + var lineHeight = charCoords.bottom - y; + switch (actionArgs.position) { + case 'center': y = y - (height / 2) + lineHeight; + break; + case 'bottom': y = y - height + lineHeight*1.4; + break; + case 'top': y = y + lineHeight*0.4; + break; + } + cm.scrollTo(null, y); + }, + replayMacro: function(cm, actionArgs) { + var registerName = actionArgs.selectedCharacter; + var repeat = actionArgs.repeat; + var macroModeState = vimGlobalState.macroModeState; + if (registerName == '@') { + registerName = macroModeState.latestRegister; + } + var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName); + while(repeat--){ + executeMacroKeyBuffer(cm, macroModeState, keyBuffer); + } + }, + exitMacroRecordMode: function() { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.toggle(); + parseKeyBufferToRegister(macroModeState.latestRegister, + macroModeState.macroKeyBuffer); + }, + enterMacroRecordMode: function(cm, actionArgs) { + var macroModeState = vimGlobalState.macroModeState; + var registerName = actionArgs.selectedCharacter; + macroModeState.toggle(cm, registerName); + emptyMacroKeyBuffer(macroModeState); + }, + enterInsertMode: function(cm, actionArgs, vim) { + if (cm.getOption('readOnly')) { return; } + vim.insertMode = true; + vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; + var insertAt = (actionArgs) ? actionArgs.insertAt : null; + if (insertAt == 'eol') { + var cursor = cm.getCursor(); + cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) }; + cm.setCursor(cursor); + } else if (insertAt == 'charAfter') { + cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); + } else if (insertAt == 'firstNonBlank') { + cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + } + cm.setOption('keyMap', 'vim-insert'); + if (actionArgs && actionArgs.replace) { + // Handle Replace-mode as a special case of insert mode. + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + if (!vimGlobalState.macroModeState.inReplay) { + // Only record if not replaying. + cm.on('change', onChange); + cm.on('cursorActivity', onCursorActivity); + CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + }, + toggleVisualMode: function(cm, actionArgs, vim) { + var repeat = actionArgs.repeat; + var curStart = cm.getCursor(); + var curEnd; + // TODO: The repeat should actually select number of characters/lines + // equal to the repeat times the size of the previous visual + // operation. + if (!vim.visualMode) { + cm.on('mousedown', exitVisualMode); + vim.visualMode = true; + vim.visualLine = !!actionArgs.linewise; + if (vim.visualLine) { + curStart.ch = 0; + curEnd = clipCursorToContent(cm, { + line: curStart.line + repeat - 1, + ch: lineLength(cm, curStart.line) + }, true /** includeLineBreak */); + } else { + curEnd = clipCursorToContent(cm, { + line: curStart.line, + ch: curStart.ch + repeat + }, true /** includeLineBreak */); + } + // Make the initial selection. + if (!actionArgs.repeatIsExplicit && !vim.visualLine) { + // This is a strange case. Here the implicit repeat is 1. The + // following commands lets the cursor hover over the 1 character + // selection. + cm.setCursor(curEnd); + cm.setSelection(curEnd, curStart); + } else { + cm.setSelection(curStart, curEnd); + } + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); + } else { + curStart = cm.getCursor('anchor'); + curEnd = cm.getCursor('head'); + if (!vim.visualLine && actionArgs.linewise) { + // Shift-V pressed in characterwise visual mode. Switch to linewise + // visual mode instead of exiting visual mode. + vim.visualLine = true; + curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 : + lineLength(cm, curStart.line); + curEnd.ch = cursorIsBefore(curStart, curEnd) ? + lineLength(cm, curEnd.line) : 0; + cm.setSelection(curStart, curEnd); + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"}); + } else if (vim.visualLine && !actionArgs.linewise) { + // v pressed in linewise visual mode. Switch to characterwise visual + // mode instead of exiting visual mode. + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); + } else { + exitVisualMode(cm); + } + } + updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart + : curEnd); + updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd + : curStart); + }, + joinLines: function(cm, actionArgs, vim) { + var curStart, curEnd; + if (vim.visualMode) { + curStart = cm.getCursor('anchor'); + curEnd = cm.getCursor('head'); + curEnd.ch = lineLength(cm, curEnd.line) - 1; + } else { + // Repeat is the number of lines to join. Minimum 2 lines. + var repeat = Math.max(actionArgs.repeat, 2); + curStart = cm.getCursor(); + curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1, + ch: Infinity }); + } + var finalCh = 0; + cm.operation(function() { + for (var i = curStart.line; i < curEnd.line; i++) { + finalCh = lineLength(cm, curStart.line); + var tmp = { line: curStart.line + 1, + ch: lineLength(cm, curStart.line + 1) }; + var text = cm.getRange(curStart, tmp); + text = text.replace(/\n\s*/g, ' '); + cm.replaceRange(text, curStart, tmp); + } + var curFinalPos = { line: curStart.line, ch: finalCh }; + cm.setCursor(curFinalPos); + }); + }, + newLineAndEnterInsertMode: function(cm, actionArgs, vim) { + vim.insertMode = true; + var insertAt = cm.getCursor(); + if (insertAt.line === cm.firstLine() && !actionArgs.after) { + // Special case for inserting newline before start of document. + cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 }); + cm.setCursor(cm.firstLine(), 0); + } else { + insertAt.line = (actionArgs.after) ? insertAt.line : + insertAt.line - 1; + insertAt.ch = lineLength(cm, insertAt.line); + cm.setCursor(insertAt); + var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || + CodeMirror.commands.newlineAndIndent; + newlineFn(cm); + } + this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); + }, + paste: function(cm, actionArgs) { + var cur = cm.getCursor(); + var register = vimGlobalState.registerController.getRegister( + actionArgs.registerName); + if (!register.text) { + return; + } + for (var text = '', i = 0; i < actionArgs.repeat; i++) { + text += register.text; + } + var linewise = register.linewise; + if (linewise) { + if (actionArgs.after) { + // Move the newline at the end to the start instead, and paste just + // before the newline character of the line we are on right now. + text = '\n' + text.slice(0, text.length - 1); + cur.ch = lineLength(cm, cur.line); + } else { + cur.ch = 0; + } + } else { + cur.ch += actionArgs.after ? 1 : 0; + } + cm.replaceRange(text, cur); + // Now fine tune the cursor to where we want it. + var curPosFinal; + var idx; + if (linewise && actionArgs.after) { + curPosFinal = { line: cur.line + 1, + ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) }; + } else if (linewise && !actionArgs.after) { + curPosFinal = { line: cur.line, + ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) }; + } else if (!linewise && actionArgs.after) { + idx = cm.indexFromPos(cur); + curPosFinal = cm.posFromIndex(idx + text.length - 1); + } else { + idx = cm.indexFromPos(cur); + curPosFinal = cm.posFromIndex(idx + text.length); + } + cm.setCursor(curPosFinal); + }, + undo: function(cm, actionArgs) { + cm.operation(function() { + repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); + cm.setCursor(cm.getCursor('anchor')); + }); + }, + redo: function(cm, actionArgs) { + repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); + }, + setRegister: function(_cm, actionArgs, vim) { + vim.inputState.registerName = actionArgs.selectedCharacter; + }, + setMark: function(cm, actionArgs, vim) { + var markName = actionArgs.selectedCharacter; + updateMark(cm, vim, markName, cm.getCursor()); + }, + replace: function(cm, actionArgs, vim) { + var replaceWith = actionArgs.selectedCharacter; + var curStart = cm.getCursor(); + var replaceTo; + var curEnd; + if(vim.visualMode){ + curStart=cm.getCursor('start'); + curEnd=cm.getCursor('end'); + // workaround to catch the character under the cursor + // existing workaround doesn't cover actions + curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1}); + }else{ + var line = cm.getLine(curStart.line); + replaceTo = curStart.ch + actionArgs.repeat; + if (replaceTo > line.length) { + replaceTo=line.length; + } + curEnd = { line: curStart.line, ch: replaceTo }; + } + if(replaceWith=='\n'){ + if(!vim.visualMode) cm.replaceRange('', curStart, curEnd); + // special case, where vim help says to replace by just one line-break + (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); + }else { + var replaceWithStr=cm.getRange(curStart, curEnd); + //replace all characters in range by selected, but keep linebreaks + replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); + cm.replaceRange(replaceWithStr, curStart, curEnd); + if(vim.visualMode){ + cm.setCursor(curStart); + exitVisualMode(cm); + }else{ + cm.setCursor(offsetCursor(curEnd, 0, -1)); + } + } + }, + incrementNumberToken: function(cm, actionArgs) { + var cur = cm.getCursor(); + var lineStr = cm.getLine(cur.line); + var re = /-?\d+/g; + var match; + var start; + var end; + var numberStr; + var token; + while ((match = re.exec(lineStr)) !== null) { + token = match[0]; + start = match.index; + end = start + token.length; + if(cur.ch < end)break; + } + if(!actionArgs.backtrack && (end <= cur.ch))return; + if (token) { + var increment = actionArgs.increase ? 1 : -1; + var number = parseInt(token) + (increment * actionArgs.repeat); + var from = {ch:start, line:cur.line}; + var to = {ch:end, line:cur.line}; + numberStr = number.toString(); + cm.replaceRange(numberStr, from, to); + } else { + return; + } + cm.setCursor({line: cur.line, ch: start + numberStr.length - 1}); + }, + repeatLastEdit: function(cm, actionArgs, vim) { + var lastEditInputState = vim.lastEditInputState; + if (!lastEditInputState) { return; } + var repeat = actionArgs.repeat; + if (repeat && actionArgs.repeatIsExplicit) { + vim.lastEditInputState.repeatOverride = repeat; + } else { + repeat = vim.lastEditInputState.repeatOverride || repeat; + } + repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); + } + }; + + var textObjects = { + // TODO: lots of possible exceptions that can be thrown here. Try da( + // outside of a () block. + // TODO: implement text objects for the reverse like }. Should just be + // an additional mapping after moving to the defaultKeyMap. + 'w': function(cm, inclusive) { + return expandWordUnderCursor(cm, inclusive, true /** forward */, + false /** bigWord */); + }, + 'W': function(cm, inclusive) { + return expandWordUnderCursor(cm, inclusive, + true /** forward */, true /** bigWord */); + }, + '{': function(cm, inclusive) { + return selectCompanionObject(cm, '}', inclusive); + }, + '(': function(cm, inclusive) { + return selectCompanionObject(cm, ')', inclusive); + }, + '[': function(cm, inclusive) { + return selectCompanionObject(cm, ']', inclusive); + }, + '\'': function(cm, inclusive) { + return findBeginningAndEnd(cm, "'", inclusive); + }, + '"': function(cm, inclusive) { + return findBeginningAndEnd(cm, '"', inclusive); + } + }; + + /* + * Below are miscellaneous utility functions used by vim.js + */ + + /** + * Clips cursor to ensure that line is within the buffer's range + * If includeLineBreak is true, then allow cur.ch == lineLength. + */ + function clipCursorToContent(cm, cur, includeLineBreak) { + var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); + var maxCh = lineLength(cm, line) - 1; + maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; + var ch = Math.min(Math.max(0, cur.ch), maxCh); + return { line: line, ch: ch }; + } + function copyArgs(args) { + var ret = {}; + for (var prop in args) { + if (args.hasOwnProperty(prop)) { + ret[prop] = args[prop]; + } + } + return ret; + } + function offsetCursor(cur, offsetLine, offsetCh) { + return { line: cur.line + offsetLine, ch: cur.ch + offsetCh }; + } + function matchKeysPartial(pressed, mapped) { + for (var i = 0; i < pressed.length; i++) { + // 'character' means any character. For mark, register commads, etc. + if (pressed[i] != mapped[i] && mapped[i] != 'character') { + return false; + } + } + return true; + } + function repeatFn(cm, fn, repeat) { + return function() { + for (var i = 0; i < repeat; i++) { + fn(cm); + } + }; + } + function copyCursor(cur) { + return { line: cur.line, ch: cur.ch }; + } + function cursorEqual(cur1, cur2) { + return cur1.ch == cur2.ch && cur1.line == cur2.line; + } + function cursorIsBefore(cur1, cur2) { + if (cur1.line < cur2.line) { + return true; + } + if (cur1.line == cur2.line && cur1.ch < cur2.ch) { + return true; + } + return false; + } + function cusrorIsBetween(cur1, cur2, cur3) { + // returns true if cur2 is between cur1 and cur3. + var cur1before2 = cursorIsBefore(cur1, cur2); + var cur2before3 = cursorIsBefore(cur2, cur3); + return cur1before2 && cur2before3; + } + function lineLength(cm, lineNum) { + return cm.getLine(lineNum).length; + } + function reverse(s){ + return s.split('').reverse().join(''); + } + function trim(s) { + if (s.trim) { + return s.trim(); + } + return s.replace(/^\s+|\s+$/g, ''); + } + function escapeRegex(s) { + return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); + } + + function exitVisualMode(cm) { + cm.off('mousedown', exitVisualMode); + var vim = cm.state.vim; + vim.visualMode = false; + vim.visualLine = false; + var selectionStart = cm.getCursor('anchor'); + var selectionEnd = cm.getCursor('head'); + if (!cursorEqual(selectionStart, selectionEnd)) { + // Clear the selection and set the cursor only if the selection has not + // already been cleared. Otherwise we risk moving the cursor somewhere + // it's not supposed to be. + cm.setCursor(clipCursorToContent(cm, selectionEnd)); + } + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + } + + // Remove any trailing newlines from the selection. For + // example, with the caret at the start of the last word on the line, + // 'dw' should word, but not the newline, while 'w' should advance the + // caret to the first character of the next line. + function clipToLine(cm, curStart, curEnd) { + var selection = cm.getRange(curStart, curEnd); + // Only clip if the selection ends with trailing newline + whitespace + if (/\n\s*$/.test(selection)) { + var lines = selection.split('\n'); + // We know this is all whitepsace. + lines.pop(); + + // Cases: + // 1. Last word is an empty line - do not clip the trailing '\n' + // 2. Last word is not an empty line - clip the trailing '\n' + var line; + // Find the line containing the last word, and clip all whitespace up + // to it. + for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { + curEnd.line--; + curEnd.ch = 0; + } + // If the last word is not an empty line, clip an additional newline + if (line) { + curEnd.line--; + curEnd.ch = lineLength(cm, curEnd.line); + } else { + curEnd.ch = 0; + } + } + } + + // Expand the selection to line ends. + function expandSelectionToLine(_cm, curStart, curEnd) { + curStart.ch = 0; + curEnd.ch = 0; + curEnd.line++; + } + + function findFirstNonWhiteSpaceCharacter(text) { + if (!text) { + return 0; + } + var firstNonWS = text.search(/\S/); + return firstNonWS == -1 ? text.length : firstNonWS; + } + + function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { + var cur = cm.getCursor(); + var line = cm.getLine(cur.line); + var idx = cur.ch; + + // Seek to first word or non-whitespace character, depending on if + // noSymbol is true. + var textAfterIdx = line.substring(idx); + var firstMatchedChar; + if (noSymbol) { + firstMatchedChar = textAfterIdx.search(/\w/); + } else { + firstMatchedChar = textAfterIdx.search(/\S/); + } + if (firstMatchedChar == -1) { + return null; + } + idx += firstMatchedChar; + textAfterIdx = line.substring(idx); + var textBeforeIdx = line.substring(0, idx); + + var matchRegex; + // Greedy matchers for the "word" we are trying to expand. + if (bigWord) { + matchRegex = /^\S+/; + } else { + if ((/\w/).test(line.charAt(idx))) { + matchRegex = /^\w+/; + } else { + matchRegex = /^[^\w\s]+/; + } + } + + var wordAfterRegex = matchRegex.exec(textAfterIdx); + var wordStart = idx; + var wordEnd = idx + wordAfterRegex[0].length; + // TODO: Find a better way to do this. It will be slow on very long lines. + var revTextBeforeIdx = reverse(textBeforeIdx); + var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); + if (wordBeforeRegex) { + wordStart -= wordBeforeRegex[0].length; + } + + if (inclusive) { + // If present, trim all whitespace after word. + // Otherwise, trim all whitespace before word. + var textAfterWordEnd = line.substring(wordEnd); + var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; + if (whitespacesAfterWord > 0) { + wordEnd += whitespacesAfterWord; + } else { + var revTrim = revTextBeforeIdx.length - wordStart; + var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); + var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; + wordStart -= whitespacesBeforeWord; + } + } + + return { start: { line: cur.line, ch: wordStart }, + end: { line: cur.line, ch: wordEnd }}; + } + + function recordJumpPosition(cm, oldCur, newCur) { + if(!cursorEqual(oldCur, newCur)) { + vimGlobalState.jumpList.add(cm, oldCur, newCur); + } + } + + function recordLastCharacterSearch(increment, args) { + vimGlobalState.lastChararacterSearch.increment = increment; + vimGlobalState.lastChararacterSearch.forward = args.forward; + vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; + } + + var symbolToMode = { + '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', + '[': 'section', ']': 'section', + '*': 'comment', '/': 'comment', + 'm': 'method', 'M': 'method', + '#': 'preprocess' + }; + var findSymbolModes = { + bracket: { + isComplete: function(state) { + if (state.nextCh === state.symb) { + state.depth++; + if(state.depth >= 1)return true; + } else if (state.nextCh === state.reverseSymb) { + state.depth--; + } + return false; + } + }, + section: { + init: function(state) { + state.curMoveThrough = true; + state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; + }, + isComplete: function(state) { + return state.index === 0 && state.nextCh === state.symb; + } + }, + comment: { + isComplete: function(state) { + var found = state.lastCh === '*' && state.nextCh === '/'; + state.lastCh = state.nextCh; + return found; + } + }, + // TODO: The original Vim implementation only operates on level 1 and 2. + // The current implementation doesn't check for code block level and + // therefore it operates on any levels. + method: { + init: function(state) { + state.symb = (state.symb === 'm' ? '{' : '}'); + state.reverseSymb = state.symb === '{' ? '}' : '{'; + }, + isComplete: function(state) { + if(state.nextCh === state.symb)return true; + return false; + } + }, + preprocess: { + init: function(state) { + state.index = 0; + }, + isComplete: function(state) { + if (state.nextCh === '#') { + var token = state.lineText.match(/#(\w+)/)[1]; + if (token === 'endif') { + if (state.forward && state.depth === 0) { + return true; + } + state.depth++; + } else if (token === 'if') { + if (!state.forward && state.depth === 0) { + return true; + } + state.depth--; + } + if(token === 'else' && state.depth === 0)return true; + } + return false; + } + } + }; + function findSymbol(cm, repeat, forward, symb) { + var cur = cm.getCursor(); + var increment = forward ? 1 : -1; + var endLine = forward ? cm.lineCount() : -1; + var curCh = cur.ch; + var line = cur.line; + var lineText = cm.getLine(line); + var state = { + lineText: lineText, + nextCh: lineText.charAt(curCh), + lastCh: null, + index: curCh, + symb: symb, + reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], + forward: forward, + depth: 0, + curMoveThrough: false + }; + var mode = symbolToMode[symb]; + if(!mode)return cur; + var init = findSymbolModes[mode].init; + var isComplete = findSymbolModes[mode].isComplete; + if(init)init(state); + while (line !== endLine && repeat) { + state.index += increment; + state.nextCh = state.lineText.charAt(state.index); + if (!state.nextCh) { + line += increment; + state.lineText = cm.getLine(line) || ''; + if (increment > 0) { + state.index = 0; + } else { + var lineLen = state.lineText.length; + state.index = (lineLen > 0) ? (lineLen-1) : 0; + } + state.nextCh = state.lineText.charAt(state.index); + } + if (isComplete(state)) { + cur.line = line; + cur.ch = state.index; + repeat--; + } + } + if (state.nextCh || state.curMoveThrough) { + return { line: line, ch: state.index }; + } + return cur; + } + + /* + * Returns the boundaries of the next word. If the cursor in the middle of + * the word, then returns the boundaries of the current word, starting at + * the cursor. If the cursor is at the start/end of a word, and we are going + * forward/backward, respectively, find the boundaries of the next word. + * + * @param {CodeMirror} cm CodeMirror object. + * @param {Cursor} cur The cursor position. + * @param {boolean} forward True to search forward. False to search + * backward. + * @param {boolean} bigWord True if punctuation count as part of the word. + * False if only [a-zA-Z0-9] characters count as part of the word. + * @param {boolean} emptyLineIsWord True if empty lines should be treated + * as words. + * @return {Object{from:number, to:number, line: number}} The boundaries of + * the word, or null if there are no more words. + */ + function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { + var lineNum = cur.line; + var pos = cur.ch; + var line = cm.getLine(lineNum); + var dir = forward ? 1 : -1; + var regexps = bigWord ? bigWordRegexp : wordRegexp; + + if (emptyLineIsWord && line == '') { + lineNum += dir; + line = cm.getLine(lineNum); + if (!isLine(cm, lineNum)) { + return null; + } + pos = (forward) ? 0 : line.length; + } + + while (true) { + if (emptyLineIsWord && line == '') { + return { from: 0, to: 0, line: lineNum }; + } + var stop = (dir > 0) ? line.length : -1; + var wordStart = stop, wordEnd = stop; + // Find bounds of next word. + while (pos != stop) { + var foundWord = false; + for (var i = 0; i < regexps.length && !foundWord; ++i) { + if (regexps[i].test(line.charAt(pos))) { + wordStart = pos; + // Advance to end of word. + while (pos != stop && regexps[i].test(line.charAt(pos))) { + pos += dir; + } + wordEnd = pos; + foundWord = wordStart != wordEnd; + if (wordStart == cur.ch && lineNum == cur.line && + wordEnd == wordStart + dir) { + // We started at the end of a word. Find the next one. + continue; + } else { + return { + from: Math.min(wordStart, wordEnd + 1), + to: Math.max(wordStart, wordEnd), + line: lineNum }; + } + } + } + if (!foundWord) { + pos += dir; + } + } + // Advance to next/prev line. + lineNum += dir; + if (!isLine(cm, lineNum)) { + return null; + } + line = cm.getLine(lineNum); + pos = (dir > 0) ? 0 : line.length; + } + // Should never get here. + throw new Error('The impossible happened.'); + } + + /** + * @param {CodeMirror} cm CodeMirror object. + * @param {int} repeat Number of words to move past. + * @param {boolean} forward True to search forward. False to search + * backward. + * @param {boolean} wordEnd True to move to end of word. False to move to + * beginning of word. + * @param {boolean} bigWord True if punctuation count as part of the word. + * False if only alphabet characters count as part of the word. + * @return {Cursor} The position the cursor should move to. + */ + function moveToWord(cm, repeat, forward, wordEnd, bigWord) { + var cur = cm.getCursor(); + var curStart = copyCursor(cur); + var words = []; + if (forward && !wordEnd || !forward && wordEnd) { + repeat++; + } + // For 'e', empty lines are not considered words, go figure. + var emptyLineIsWord = !(forward && wordEnd); + for (var i = 0; i < repeat; i++) { + var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); + if (!word) { + var eodCh = lineLength(cm, cm.lastLine()); + words.push(forward + ? {line: cm.lastLine(), from: eodCh, to: eodCh} + : {line: 0, from: 0, to: 0}); + break; + } + words.push(word); + cur = {line: word.line, ch: forward ? (word.to - 1) : word.from}; + } + var shortCircuit = words.length != repeat; + var firstWord = words[0]; + var lastWord = words.pop(); + if (forward && !wordEnd) { + // w + if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return {line: lastWord.line, ch: lastWord.from}; + } else if (forward && wordEnd) { + return {line: lastWord.line, ch: lastWord.to - 1}; + } else if (!forward && wordEnd) { + // ge + if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return {line: lastWord.line, ch: lastWord.to}; + } else { + // b + return {line: lastWord.line, ch: lastWord.from}; + } + } + + function moveToCharacter(cm, repeat, forward, character) { + var cur = cm.getCursor(); + var start = cur.ch; + var idx; + for (var i = 0; i < repeat; i ++) { + var line = cm.getLine(cur.line); + idx = charIdxInLine(start, line, character, forward, true); + if (idx == -1) { + return null; + } + start = idx; + } + return { line: cm.getCursor().line, ch: idx }; + } + + function moveToColumn(cm, repeat) { + // repeat is always >= 1, so repeat - 1 always corresponds + // to the column we want to go to. + var line = cm.getCursor().line; + return clipCursorToContent(cm, { line: line, ch: repeat - 1 }); + } + + function updateMark(cm, vim, markName, pos) { + if (!inArray(markName, validMarks)) { + return; + } + if (vim.marks[markName]) { + vim.marks[markName].clear(); + } + vim.marks[markName] = cm.setBookmark(pos); + } + + function charIdxInLine(start, line, character, forward, includeChar) { + // Search for char in line. + // motion_options: {forward, includeChar} + // If includeChar = true, include it too. + // If forward = true, search forward, else search backwards. + // If char is not found on this line, do nothing + var idx; + if (forward) { + idx = line.indexOf(character, start + 1); + if (idx != -1 && !includeChar) { + idx -= 1; + } + } else { + idx = line.lastIndexOf(character, start - 1); + if (idx != -1 && !includeChar) { + idx += 1; + } + } + return idx; + } + + function getContextLevel(ctx) { + return (ctx === 'string' || ctx === 'comment') ? 1 : 0; + } + + function findMatchedSymbol(cm, cur, symb) { + var line = cur.line; + var ch = cur.ch; + symb = symb ? symb : cm.getLine(line).charAt(ch); + + var symbContext = cm.getTokenAt({line:line, ch:ch+1}).type; + var symbCtxLevel = getContextLevel(symbContext); + + var reverseSymb = ({ + '(': ')', ')': '(', + '[': ']', ']': '[', + '{': '}', '}': '{'})[symb]; + + // Couldn't find a matching symbol, abort + if (!reverseSymb) { + return cur; + } + + // set our increment to move forward (+1) or backwards (-1) + // depending on which bracket we're matching + var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1; + var endLine = increment === 1 ? cm.lineCount() : -1; + var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line); + // Simple search for closing paren--just count openings and closings till + // we find our match + // TODO: use info from CodeMirror to ignore closing brackets in comments + // and quotes, etc. + while (line !== endLine && depth > 0) { + index += increment; + nextCh = lineText.charAt(index); + if (!nextCh) { + line += increment; + lineText = cm.getLine(line) || ''; + if (increment > 0) { + index = 0; + } else { + var lineLen = lineText.length; + index = (lineLen > 0) ? (lineLen-1) : 0; + } + nextCh = lineText.charAt(index); + } + var revSymbContext = cm.getTokenAt({line:line, ch:index+1}).type; + var revSymbCtxLevel = getContextLevel(revSymbContext); + if (symbCtxLevel >= revSymbCtxLevel) { + if (nextCh === symb) { + depth++; + } else if (nextCh === reverseSymb) { + depth--; + } + } + } + + if (nextCh) { + return { line: line, ch: index }; + } + return cur; + } + + function selectCompanionObject(cm, revSymb, inclusive) { + var cur = cm.getCursor(); + + var end = findMatchedSymbol(cm, cur, revSymb); + var start = findMatchedSymbol(cm, end); + start.ch += inclusive ? 1 : 0; + end.ch += inclusive ? 0 : 1; + + return { start: start, end: end }; + } + + // Takes in a symbol and a cursor and tries to simulate text objects that + // have identical opening and closing symbols + // TODO support across multiple lines + function findBeginningAndEnd(cm, symb, inclusive) { + var cur = cm.getCursor(); + var line = cm.getLine(cur.line); + var chars = line.split(''); + var start, end, i, len; + var firstIndex = chars.indexOf(symb); + + // the decision tree is to always look backwards for the beginning first, + // but if the cursor is in front of the first instance of the symb, + // then move the cursor forward + if (cur.ch < firstIndex) { + cur.ch = firstIndex; + // Why is this line even here??? + // cm.setCursor(cur.line, firstIndex+1); + } + // otherwise if the cursor is currently on the closing symbol + else if (firstIndex < cur.ch && chars[cur.ch] == symb) { + end = cur.ch; // assign end to the current cursor + --cur.ch; // make sure to look backwards + } + + // if we're currently on the symbol, we've got a start + if (chars[cur.ch] == symb && !end) { + start = cur.ch + 1; // assign start to ahead of the cursor + } else { + // go backwards to find the start + for (i = cur.ch; i > -1 && !start; i--) { + if (chars[i] == symb) { + start = i + 1; + } + } + } + + // look forwards for the end symbol + if (start && !end) { + for (i = start, len = chars.length; i < len && !end; i++) { + if (chars[i] == symb) { + end = i; + } + } + } + + // nothing found + if (!start || !end) { + return { start: cur, end: cur }; + } + + // include the symbols + if (inclusive) { + --start; ++end; + } + + return { + start: { line: cur.line, ch: start }, + end: { line: cur.line, ch: end } + }; + } + + // Search functions + function SearchState() {} + SearchState.prototype = { + getQuery: function() { + return vimGlobalState.query; + }, + setQuery: function(query) { + vimGlobalState.query = query; + }, + getOverlay: function() { + return this.searchOverlay; + }, + setOverlay: function(overlay) { + this.searchOverlay = overlay; + }, + isReversed: function() { + return vimGlobalState.isReversed; + }, + setReversed: function(reversed) { + vimGlobalState.isReversed = reversed; + } + }; + function getSearchState(cm) { + var vim = cm.state.vim; + return vim.searchState_ || (vim.searchState_ = new SearchState()); + } + function dialog(cm, template, shortText, onClose, options) { + if (cm.openDialog) { + cm.openDialog(template, onClose, { bottom: true, value: options.value, + onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp }); + } + else { + onClose(prompt(shortText, '')); + } + } + + function findUnescapedSlashes(str) { + var escapeNextChar = false; + var slashes = []; + for (var i = 0; i < str.length; i++) { + var c = str.charAt(i); + if (!escapeNextChar && c == '/') { + slashes.push(i); + } + escapeNextChar = (c == '\\'); + } + return slashes; + } + /** + * Extract the regular expression from the query and return a Regexp object. + * Returns null if the query is blank. + * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. + * If smartCase is passed in, and the query contains upper case letters, + * then ignoreCase is overridden, and the 'i' flag will not be set. + * If the query contains the /i in the flag part of the regular expression, + * then both ignoreCase and smartCase are ignored, and 'i' will be passed + * through to the Regex object. + */ + function parseQuery(query, ignoreCase, smartCase) { + // Check if the query is already a regex. + if (query instanceof RegExp) { return query; } + // First try to extract regex + flags from the input. If no flags found, + // extract just the regex. IE does not accept flags directly defined in + // the regex string in the form /regex/flags + var slashes = findUnescapedSlashes(query); + var regexPart; + var forceIgnoreCase; + if (!slashes.length) { + // Query looks like 'regexp' + regexPart = query; + } else { + // Query looks like 'regexp/...' + regexPart = query.substring(0, slashes[0]); + var flagsPart = query.substring(slashes[0]); + forceIgnoreCase = (flagsPart.indexOf('i') != -1); + } + if (!regexPart) { + return null; + } + if (smartCase) { + ignoreCase = (/^[^A-Z]*$/).test(regexPart); + } + var regexp = new RegExp(regexPart, + (ignoreCase || forceIgnoreCase) ? 'i' : undefined); + return regexp; + } + function showConfirm(cm, text) { + if (cm.openConfirm) { + cm.openConfirm('' + text + + ' ', function() {}, + {bottom: true}); + } else { + alert(text); + } + } + function makePrompt(prefix, desc) { + var raw = ''; + if (prefix) { + raw += '' + prefix + ''; + } + raw += ' ' + + ''; + if (desc) { + raw += ''; + raw += desc; + raw += ''; + } + return raw; + } + var searchPromptDesc = '(Javascript regexp)'; + function showPrompt(cm, options) { + var shortText = (options.prefix || '') + ' ' + (options.desc || ''); + var prompt = makePrompt(options.prefix, options.desc); + dialog(cm, prompt, shortText, options.onClose, options); + } + function regexEqual(r1, r2) { + if (r1 instanceof RegExp && r2 instanceof RegExp) { + var props = ['global', 'multiline', 'ignoreCase', 'source']; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (r1[prop] !== r2[prop]) { + return false; + } + } + return true; + } + return false; + } + // Returns true if the query is valid. + function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { + if (!rawQuery) { + return; + } + var state = getSearchState(cm); + var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); + if (!query) { + return; + } + highlightSearchMatches(cm, query); + if (regexEqual(query, state.getQuery())) { + return query; + } + state.setQuery(query); + return query; + } + function searchOverlay(query) { + if (query.source.charAt(0) == '^') { + var matchSol = true; + } + return { + token: function(stream) { + if (matchSol && !stream.sol()) { + stream.skipToEnd(); + return; + } + var match = stream.match(query, false); + if (match) { + if (match[0].length == 0) { + // Matched empty string, skip to next. + stream.next(); + return 'searching'; + } + if (!stream.sol()) { + // Backtrack 1 to match \b + stream.backUp(1); + if (!query.exec(stream.next() + match[0])) { + stream.next(); + return null; + } + } + stream.match(query); + return 'searching'; + } + while (!stream.eol()) { + stream.next(); + if (stream.match(query, false)) break; + } + }, + query: query + }; + } + function highlightSearchMatches(cm, query) { + var overlay = getSearchState(cm).getOverlay(); + if (!overlay || query != overlay.query) { + if (overlay) { + cm.removeOverlay(overlay); + } + overlay = searchOverlay(query); + cm.addOverlay(overlay); + getSearchState(cm).setOverlay(overlay); + } + } + function findNext(cm, prev, query, repeat) { + if (repeat === undefined) { repeat = 1; } + return cm.operation(function() { + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); + for (var i = 0; i < repeat; i++) { + var found = cursor.find(prev); + if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + (prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} ); + if (!cursor.find(prev)) { + return; + } + } + } + return cursor.from(); + }); + } + function clearSearchHighlight(cm) { + cm.removeOverlay(getSearchState(cm).getOverlay()); + getSearchState(cm).setOverlay(null); + } + /** + * Check if pos is in the specified range, INCLUSIVE. + * Range can be specified with 1 or 2 arguments. + * If the first range argument is an array, treat it as an array of line + * numbers. Match pos against any of the lines. + * If the first range argument is a number, + * if there is only 1 range argument, check if pos has the same line + * number + * if there are 2 range arguments, then check if pos is in between the two + * range arguments. + */ + function isInRange(pos, start, end) { + if (typeof pos != 'number') { + // Assume it is a cursor position. Get the line number. + pos = pos.line; + } + if (start instanceof Array) { + return inArray(pos, start); + } else { + if (end) { + return (pos >= start && pos <= end); + } else { + return pos == start; + } + } + } + function getUserVisibleLines(cm) { + var scrollInfo = cm.getScrollInfo(); + var occludeToleranceTop = 6; + var occludeToleranceBottom = 10; + var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); + var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; + var to = cm.coordsChar({left:0, top: bottomY}, 'local'); + return {top: from.line, bottom: to.line}; + } + + // Ex command handling + // Care must be taken when adding to the default Ex command map. For any + // pair of commands that have a shared prefix, at least one of their + // shortNames must not match the prefix of the other command. + var defaultExCommandMap = [ + { name: 'map', type: 'builtIn' }, + { name: 'write', shortName: 'w', type: 'builtIn' }, + { name: 'undo', shortName: 'u', type: 'builtIn' }, + { name: 'redo', shortName: 'red', type: 'builtIn' }, + { name: 'sort', shortName: 'sor', type: 'builtIn'}, + { name: 'substitute', shortName: 's', type: 'builtIn'}, + { name: 'nohlsearch', shortName: 'noh', type: 'builtIn'}, + { name: 'delmarks', shortName: 'delm', type: 'builtin'} + ]; + Vim.ExCommandDispatcher = function() { + this.buildCommandMap_(); + }; + Vim.ExCommandDispatcher.prototype = { + processCommand: function(cm, input) { + var vim = cm.state.vim; + if (vim.visualMode) { + exitVisualMode(cm); + } + var inputStream = new CodeMirror.StringStream(input); + var params = {}; + params.input = input; + try { + this.parseInput_(cm, inputStream, params); + } catch(e) { + showConfirm(cm, e); + return; + } + var commandName; + if (!params.commandName) { + // If only a line range is defined, move to the line. + if (params.line !== undefined) { + commandName = 'move'; + } + } else { + var command = this.matchCommand_(params.commandName); + if (command) { + commandName = command.name; + this.parseCommandArgs_(inputStream, params, command); + if (command.type == 'exToKey') { + // Handle Ex to Key mapping. + for (var i = 0; i < command.toKeys.length; i++) { + CodeMirror.Vim.handleKey(cm, command.toKeys[i]); + } + return; + } else if (command.type == 'exToEx') { + // Handle Ex to Ex mapping. + this.processCommand(cm, command.toInput); + return; + } + } + } + if (!commandName) { + showConfirm(cm, 'Not an editor command ":' + input + '"'); + return; + } + try { + exCommands[commandName](cm, params); + } catch(e) { + showConfirm(cm, e); + } + }, + parseInput_: function(cm, inputStream, result) { + inputStream.eatWhile(':'); + // Parse range. + if (inputStream.eat('%')) { + result.line = cm.firstLine(); + result.lineEnd = cm.lastLine(); + } else { + result.line = this.parseLineSpec_(cm, inputStream); + if (result.line !== undefined && inputStream.eat(',')) { + result.lineEnd = this.parseLineSpec_(cm, inputStream); + } + } + + // Parse command name. + var commandMatch = inputStream.match(/^(\w+)/); + if (commandMatch) { + result.commandName = commandMatch[1]; + } else { + result.commandName = inputStream.match(/.*/)[0]; + } + + return result; + }, + parseLineSpec_: function(cm, inputStream) { + var numberMatch = inputStream.match(/^(\d+)/); + if (numberMatch) { + return parseInt(numberMatch[1], 10) - 1; + } + switch (inputStream.next()) { + case '.': + return cm.getCursor().line; + case '$': + return cm.lastLine(); + case '\'': + var mark = cm.state.vim.marks[inputStream.next()]; + if (mark && mark.find()) { + return mark.find().line; + } + throw new Error('Mark not set'); + default: + inputStream.backUp(1); + return undefined; + } + }, + parseCommandArgs_: function(inputStream, params, command) { + if (inputStream.eol()) { + return; + } + params.argString = inputStream.match(/.*/)[0]; + // Parse command-line arguments + var delim = command.argDelimiter || /\s+/; + var args = trim(params.argString).split(delim); + if (args.length && args[0]) { + params.args = args; + } + }, + matchCommand_: function(commandName) { + // Return the command in the command map that matches the shortest + // prefix of the passed in command name. The match is guaranteed to be + // unambiguous if the defaultExCommandMap's shortNames are set up + // correctly. (see @code{defaultExCommandMap}). + for (var i = commandName.length; i > 0; i--) { + var prefix = commandName.substring(0, i); + if (this.commandMap_[prefix]) { + var command = this.commandMap_[prefix]; + if (command.name.indexOf(commandName) === 0) { + return command; + } + } + } + return null; + }, + buildCommandMap_: function() { + this.commandMap_ = {}; + for (var i = 0; i < defaultExCommandMap.length; i++) { + var command = defaultExCommandMap[i]; + var key = command.shortName || command.name; + this.commandMap_[key] = command; + } + }, + map: function(lhs, rhs) { + if (lhs != ':' && lhs.charAt(0) == ':') { + var commandName = lhs.substring(1); + if (rhs != ':' && rhs.charAt(0) == ':') { + // Ex to Ex mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToEx', + toInput: rhs.substring(1) + }; + } else { + // Ex to key mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToKey', + toKeys: parseKeyString(rhs) + }; + } + } else { + if (rhs != ':' && rhs.charAt(0) == ':') { + // Key to Ex mapping. + defaultKeymap.unshift({ + keys: parseKeyString(lhs), + type: 'keyToEx', + exArgs: { input: rhs.substring(1) }}); + } else { + // Key to key mapping + defaultKeymap.unshift({ + keys: parseKeyString(lhs), + type: 'keyToKey', + toKeys: parseKeyString(rhs) + }); + } + } + } + }; + + // Converts a key string sequence of the form abd into Vim's + // keymap representation. + function parseKeyString(str) { + var key, match; + var keys = []; + while (str) { + match = (/<\w+-.+?>|<\w+>|./).exec(str); + if(match === null)break; + key = match[0]; + str = str.substring(match.index + key.length); + keys.push(key); + } + return keys; + } + + var exCommands = { + map: function(cm, params) { + var mapArgs = params.args; + if (!mapArgs || mapArgs.length < 2) { + if (cm) { + showConfirm(cm, 'Invalid mapping: ' + params.input); + } + return; + } + exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm); + }, + move: function(cm, params) { + commandDispatcher.processCommand(cm, cm.state.vim, { + type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, + linewise: true }, + repeatOverride: params.line+1}); + }, + sort: function(cm, params) { + var reverse, ignoreCase, unique, number; + function parseArgs() { + if (params.argString) { + var args = new CodeMirror.StringStream(params.argString); + if (args.eat('!')) { reverse = true; } + if (args.eol()) { return; } + if (!args.eatSpace()) { throw new Error('invalid arguments ' + args.match(/.*/)[0]); } + var opts = args.match(/[a-z]+/); + if (opts) { + opts = opts[0]; + ignoreCase = opts.indexOf('i') != -1; + unique = opts.indexOf('u') != -1; + var decimal = opts.indexOf('d') != -1 && 1; + var hex = opts.indexOf('x') != -1 && 1; + var octal = opts.indexOf('o') != -1 && 1; + if (decimal + hex + octal > 1) { throw new Error('invalid arguments'); } + number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; + } + if (args.eatSpace() && args.match(/\/.*\//)) { throw new Error('patterns not supported'); } + } + } + parseArgs(); + var lineStart = params.line || cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + if (lineStart == lineEnd) { return; } + var curStart = { line: lineStart, ch: 0 }; + var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) }; + var text = cm.getRange(curStart, curEnd).split('\n'); + var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : + (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : + (number == 'octal') ? /([0-7]+)/ : null; + var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; + var numPart = [], textPart = []; + if (number) { + for (var i = 0; i < text.length; i++) { + if (numberRegex.exec(text[i])) { + numPart.push(text[i]); + } else { + textPart.push(text[i]); + } + } + } else { + textPart = text; + } + function compareFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } + var anum = number && numberRegex.exec(a); + var bnum = number && numberRegex.exec(b); + if (!anum) { return a < b ? -1 : 1; } + anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); + bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); + return anum - bnum; + } + numPart.sort(compareFn); + textPart.sort(compareFn); + text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); + if (unique) { // Remove duplicate lines + var textOld = text; + var lastLine; + text = []; + for (var i = 0; i < textOld.length; i++) { + if (textOld[i] != lastLine) { + text.push(textOld[i]); + } + lastLine = textOld[i]; + } + } + cm.replaceRange(text.join('\n'), curStart, curEnd); + }, + substitute: function(cm, params) { + if (!cm.getSearchCursor) { + throw new Error('Search feature not available. Requires searchcursor.js or ' + + 'any other getSearchCursor implementation.'); + } + var argString = params.argString; + var slashes = findUnescapedSlashes(argString); + if (slashes[0] !== 0) { + showConfirm(cm, 'Substitutions should be of the form ' + + ':s/pattern/replace/'); + return; + } + var regexPart = argString.substring(slashes[0] + 1, slashes[1]); + var replacePart = ''; + var flagsPart; + var count; + var confirm = false; // Whether to confirm each replace. + if (slashes[1]) { + replacePart = argString.substring(slashes[1] + 1, slashes[2]); + } + if (slashes[2]) { + // After the 3rd slash, we can have flags followed by a space followed + // by count. + var trailing = argString.substring(slashes[2] + 1).split(' '); + flagsPart = trailing[0]; + count = parseInt(trailing[1]); + } + if (flagsPart) { + if (flagsPart.indexOf('c') != -1) { + confirm = true; + flagsPart.replace('c', ''); + } + regexPart = regexPart + '/' + flagsPart; + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise use + // the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); + return; + } + } + var state = getSearchState(cm); + var query = state.getQuery(); + var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; + var lineEnd = params.lineEnd || lineStart; + if (count) { + lineStart = lineEnd; + lineEnd = lineStart + count - 1; + } + var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 }); + var cursor = cm.getSearchCursor(query, startPos); + doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); + }, + redo: CodeMirror.commands.redo, + undo: CodeMirror.commands.undo, + write: function(cm) { + if (CodeMirror.commands.save) { + // If a save command is defined, call it. + CodeMirror.commands.save(cm); + } else { + // Saves to text area if no save command is defined. + cm.save(); + } + }, + nohlsearch: function(cm) { + clearSearchHighlight(cm); + }, + delmarks: function(cm, params) { + if (!params.argString || !params.argString.trim()) { + showConfirm(cm, 'Argument required'); + return; + } + + var state = cm.state.vim; + var stream = new CodeMirror.StringStream(params.argString.trim()); + while (!stream.eol()) { + stream.eatSpace(); + + // Record the streams position at the beginning of the loop for use + // in error messages. + var count = stream.pos; + + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var sym = stream.next(); + // Check if this symbol is part of a range + if (stream.match('-', true)) { + // This symbol is part of a range. + + // The range must terminate at an alphabetic character. + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var startMark = sym; + var finishMark = stream.next(); + // The range must terminate at an alphabetic character which + // shares the same case as the start of the range. + if (isLowerCase(startMark) && isLowerCase(finishMark) || + isUpperCase(startMark) && isUpperCase(finishMark)) { + var start = startMark.charCodeAt(0); + var finish = finishMark.charCodeAt(0); + if (start >= finish) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + // Because marks are always ASCII values, and we have + // determined that they are the same case, we can use + // their char codes to iterate through the defined range. + for (var j = 0; j <= finish - start; j++) { + var mark = String.fromCharCode(start + j); + delete state.marks[mark]; + } + } else { + showConfirm(cm, 'Invalid argument: ' + startMark + '-'); + return; + } + } else { + // This symbol is a valid mark, and is not part of a range. + delete state.marks[sym]; + } + } + } + }; + + var exCommandDispatcher = new Vim.ExCommandDispatcher(); + + /** + * @param {CodeMirror} cm CodeMirror instance we are in. + * @param {boolean} confirm Whether to confirm each replace. + * @param {Cursor} lineStart Line to start replacing from. + * @param {Cursor} lineEnd Line to stop replacing at. + * @param {RegExp} query Query for performing matches with. + * @param {string} replaceWith Text to replace matches with. May contain $1, + * $2, etc for replacing captured groups using Javascript replace. + */ + function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query, + replaceWith) { + // Set up all the functions. + cm.state.vim.exMode = true; + var done = false; + var lastPos = searchCursor.from(); + function replaceAll() { + cm.operation(function() { + while (!done) { + replace(); + next(); + } + stop(); + }); + } + function replace() { + var text = cm.getRange(searchCursor.from(), searchCursor.to()); + var newText = text.replace(query, replaceWith); + searchCursor.replace(newText); + } + function next() { + var found = searchCursor.findNext(); + if (!found) { + done = true; + } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) { + cm.scrollIntoView(searchCursor.from(), 30); + cm.setSelection(searchCursor.from(), searchCursor.to()); + lastPos = searchCursor.from(); + done = false; + } else { + done = true; + } + } + function stop(close) { + if (close) { close(); } + cm.focus(); + if (lastPos) { + cm.setCursor(lastPos); + var vim = cm.state.vim; + vim.exMode = false; + vim.lastHPos = vim.lastHSPos = lastPos.ch; + } + } + function onPromptKeyDown(e, _value, close) { + // Swallow all keys. + CodeMirror.e_stop(e); + var keyName = CodeMirror.keyName(e); + switch (keyName) { + case 'Y': + replace(); next(); break; + case 'N': + next(); break; + case 'A': + cm.operation(replaceAll); break; + case 'L': + replace(); + // fall through and exit. + case 'Q': + case 'Esc': + case 'Ctrl-C': + case 'Ctrl-[': + stop(close); + break; + } + if (done) { stop(close); } + } + + // Actually do replace. + next(); + if (done) { + throw new Error('No matches for ' + query.source); + } + if (!confirm) { + replaceAll(); + return; + } + showPrompt(cm, { + prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)', + onKeyDown: onPromptKeyDown + }); + } + + // Register Vim with CodeMirror + function buildVimKeyMap() { + /** + * Handle the raw key event from CodeMirror. Translate the + * Shift + key modifier to the resulting letter, while preserving other + * modifers. + */ + // TODO: Figure out a way to catch capslock. + function cmKeyToVimKey(key, modifier) { + var vimKey = key; + if (isUpperCase(vimKey)) { + // Convert to lower case if shift is not the modifier since the key + // we get from CodeMirror is always upper case. + if (modifier == 'Shift') { + modifier = null; + } + else { + vimKey = vimKey.toLowerCase(); + } + } + if (modifier) { + // Vim will parse modifier+key combination as a single key. + vimKey = modifier.charAt(0) + '-' + vimKey; + } + var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey]; + vimKey = specialKey ? specialKey : vimKey; + vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey; + return vimKey; + } + + // Closure to bind CodeMirror, key, modifier. + function keyMapper(vimKey) { + return function(cm) { + CodeMirror.Vim.handleKey(cm, vimKey); + }; + } + + var cmToVimKeymap = { + 'nofallthrough': true, + 'disableInput': true, + 'style': 'fat-cursor' + }; + function bindKeys(keys, modifier) { + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!modifier && inArray(key, specialSymbols)) { + // Wrap special symbols with '' because that's how CodeMirror binds + // them. + key = "'" + key + "'"; + } + var vimKey = cmKeyToVimKey(keys[i], modifier); + var cmKey = modifier ? modifier + '-' + key : key; + cmToVimKeymap[cmKey] = keyMapper(vimKey); + } + } + bindKeys(upperCaseAlphabet); + bindKeys(upperCaseAlphabet, 'Shift'); + bindKeys(upperCaseAlphabet, 'Ctrl'); + bindKeys(specialSymbols); + bindKeys(specialSymbols, 'Ctrl'); + bindKeys(numbers); + bindKeys(numbers, 'Ctrl'); + bindKeys(specialKeys); + bindKeys(specialKeys, 'Ctrl'); + return cmToVimKeymap; + } + CodeMirror.keyMap.vim = buildVimKeyMap(); + + function exitInsertMode(cm) { + var vim = cm.state.vim; + var inReplay = vimGlobalState.macroModeState.inReplay; + if (!inReplay) { + cm.off('change', onChange); + cm.off('cursorActivity', onCursorActivity); + CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (!inReplay && vim.insertModeRepeat > 1) { + // Perform insert mode repeat for commands like 3,a and 3,o. + repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, + true /** repeatForInsert */); + vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; + } + delete vim.insertModeRepeat; + cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true); + vim.insertMode = false; + cm.setOption('keyMap', 'vim'); + cm.toggleOverwrite(false); // exit replace mode if we were in it. + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + } + + CodeMirror.keyMap['vim-insert'] = { + // TODO: override navigation keys so that Esc will cancel automatic + // indentation from o, O, i_ + 'Esc': exitInsertMode, + 'Ctrl-[': exitInsertMode, + 'Ctrl-C': exitInsertMode, + 'Ctrl-N': 'autocomplete', + 'Ctrl-P': 'autocomplete', + 'Enter': function(cm) { + var fn = CodeMirror.commands.newlineAndIndentContinueComment || + CodeMirror.commands.newlineAndIndent; + fn(cm); + }, + fallthrough: ['default'] + }; + + CodeMirror.keyMap['vim-replace'] = { + 'Backspace': 'goCharLeft', + fallthrough: ['vim-insert'] + }; + + function parseRegisterToKeyBuffer(macroModeState, registerName) { + var match, key; + var register = vimGlobalState.registerController.getRegister(registerName); + var text = register.toString(); + var macroKeyBuffer = macroModeState.macroKeyBuffer; + emptyMacroKeyBuffer(macroModeState); + do { + match = (/<\w+-.+?>|<\w+>|./).exec(text); + if(match === null)break; + key = match[0]; + text = text.substring(match.index + key.length); + macroKeyBuffer.push(key); + } while (text); + return macroKeyBuffer; + } + + function parseKeyBufferToRegister(registerName, keyBuffer) { + var text = keyBuffer.join(''); + vimGlobalState.registerController.setRegisterText(registerName, text); + } + + function emptyMacroKeyBuffer(macroModeState) { + if(macroModeState.isMacroPlaying)return; + var macroKeyBuffer = macroModeState.macroKeyBuffer; + macroKeyBuffer.length = 0; + } + + function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) { + macroModeState.isMacroPlaying = true; + for (var i = 0, len = keyBuffer.length; i < len; i++) { + CodeMirror.Vim.handleKey(cm, keyBuffer[i]); + }; + macroModeState.isMacroPlaying = false; + } + + function logKey(macroModeState, key) { + if(macroModeState.isMacroPlaying)return; + var macroKeyBuffer = macroModeState.macroKeyBuffer; + macroKeyBuffer.push(key); + } + + /** + * Listens for changes made in insert mode. + * Should only be active in insert mode. + */ + function onChange(_cm, changeObj) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + while (changeObj) { + lastChange.expectCursorActivityForChange = true; + if (changeObj.origin == '+input' || changeObj.origin == 'paste' + || changeObj.origin === undefined /* only in testing */) { + var text = changeObj.text.join('\n'); + lastChange.changes.push(text); + } + // Change objects may be chained with next. + changeObj = changeObj.next; + } + } + + /** + * Listens for any kind of cursor activity on CodeMirror. + * - For tracking cursor activity in insert mode. + * - Should only be active in insert mode. + */ + function onCursorActivity() { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + if (lastChange.expectCursorActivityForChange) { + lastChange.expectCursorActivityForChange = false; + } else { + // Cursor moved outside the context of an edit. Reset the change. + lastChange.changes = []; + } + } + + /** Wrapper for special keys pressed in insert mode */ + function InsertModeKey(keyName) { + this.keyName = keyName; + } + + /** + * Handles raw key down events from the text area. + * - Should only be active in insert mode. + * - For recording deletes in insert mode. + */ + function onKeyEventTargetKeyDown(e) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + var keyName = CodeMirror.keyName(e); + function onKeyFound() { + lastChange.changes.push(new InsertModeKey(keyName)); + return true; + } + if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { + CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound); + } + } + + /** + * Repeats the last edit, which includes exactly 1 command and at most 1 + * insert. Operator and motion commands are read from lastEditInputState, + * while action commands are read from lastEditActionCommand. + * + * If repeatForInsert is true, then the function was called by + * exitInsertMode to repeat the insert mode changes the user just made. The + * corresponding enterInsertMode call was made with a count. + */ + function repeatLastEdit(cm, vim, repeat, repeatForInsert) { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.inReplay = true; + var isAction = !!vim.lastEditActionCommand; + var cachedInputState = vim.inputState; + function repeatCommand() { + if (isAction) { + commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); + } else { + commandDispatcher.evalInput(cm, vim); + } + } + function repeatInsert(repeat) { + if (macroModeState.lastInsertModeChanges.changes.length > 0) { + // For some reason, repeat cw in desktop VIM will does not repeat + // insert mode changes. Will conform to that behavior. + repeat = !vim.lastEditActionCommand ? 1 : repeat; + repeatLastInsertModeChanges(cm, repeat, macroModeState); + } + } + vim.inputState = vim.lastEditInputState; + if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { + // o and O repeat have to be interlaced with insert repeats so that the + // insertions appear on separate lines instead of the last line. + for (var i = 0; i < repeat; i++) { + repeatCommand(); + repeatInsert(1); + } + } else { + if (!repeatForInsert) { + // Hack to get the cursor to end up at the right place. If I is + // repeated in insert mode repeat, cursor will be 1 insert + // change set left of where it should be. + repeatCommand(); + } + repeatInsert(repeat); + } + vim.inputState = cachedInputState; + if (vim.insertMode && !repeatForInsert) { + // Don't exit insert mode twice. If repeatForInsert is set, then we + // were called by an exitInsertMode call lower on the stack. + exitInsertMode(cm); + } + macroModeState.inReplay = false; + }; + + function repeatLastInsertModeChanges(cm, repeat, macroModeState) { + var lastChange = macroModeState.lastInsertModeChanges; + function keyHandler(binding) { + if (typeof binding == 'string') { + CodeMirror.commands[binding](cm); + } else { + binding(cm); + } + return true; + } + for (var i = 0; i < repeat; i++) { + for (var j = 0; j < lastChange.changes.length; j++) { + var change = lastChange.changes[j]; + if (change instanceof InsertModeKey) { + CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); + } else { + var cur = cm.getCursor(); + cm.replaceRange(change, cur, cur); + } + } + } + } + + resetVimGlobalState(); + return vimApi; + }; + // Initialize Vim and make it available as an API. + CodeMirror.Vim = Vim(); +} +)(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.css new file mode 100644 index 000000000..23eaf74d4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.css @@ -0,0 +1,263 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; +} +.CodeMirror-scroll { + /* Set scrolling behaviour here */ + overflow: auto; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; +} + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; + z-index: 3; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; + z-index: 1; +} +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} + +.cm-tab { display: inline-block; } + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable {color: black;} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-property {color: black;} +.cm-s-default .cm-operator {color: black;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + line-height: 1; + position: relative; + overflow: hidden; + background: white; + color: black; +} + +.CodeMirror-scroll { + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; padding-right: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.CodeMirror-sizer { + position: relative; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + padding-bottom: 30px; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + -moz-box-sizing: content-box; + box-sizing: content-box; + padding-bottom: 30px; + margin-bottom: -32px; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} + +.CodeMirror-lines { + cursor: text; +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} +.CodeMirror-code pre { + border-right: 30px solid transparent; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} +.CodeMirror-wrap .CodeMirror-code pre { + border-right: none; + width: auto; +} +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-wrap .CodeMirror-scroll { + overflow-x: hidden; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + visibility: hidden; + border-right: none; + width: 0; +} +.CodeMirror-focused div.CodeMirror-cursor { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursor { + visibility: hidden; + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.js new file mode 100644 index 000000000..7b48bf5fc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/lib/codemirror.js @@ -0,0 +1,5910 @@ +// CodeMirror version 3.19 +// +// CodeMirror is the only global var we claim +window.CodeMirror = (function() { + "use strict"; + + // BROWSER SNIFFING + + // Crude, but necessary to handle a number of hard-to-feature-detect + // bugs and behavior differences. + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8); + var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var opera = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (opera_version) opera_version = Number(opera_version[1]); + if (opera_version && opera_version >= 15) { opera = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11)); + var captureMiddleClick = gecko || (ie && !ie_lt9); + + // Optimize some code when these features are not used + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // CONSTRUCTOR + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options || {}; + // Determine effective options based on given values and defaults. + for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) + options[opt] = defaults[opt]; + setGuttersForLineNumbers(options); + + var docStart = typeof options.value == "string" ? 0 : options.value.first; + var display = this.display = makeDisplay(place, docStart); + display.wrapper.CodeMirror = this; + updateGutters(this); + if (options.autofocus && !mobile) focusInput(this); + + this.state = {keyMaps: [], + overlays: [], + modeGen: 0, + overwrite: false, focused: false, + suppressEdits: false, pasteIncoming: false, + draggingText: false, + highlight: new Delayed()}; + + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(options.value, options.mode); + operation(this, attachDoc)(this, doc); + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie) setTimeout(bind(resetInput, this, true), 20); + + registerEventHandlers(this); + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { } + if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20); + else onBlur(this); + + operation(this, function() { + for (var opt in optionHandlers) + if (optionHandlers.propertyIsEnumerable(opt)) + optionHandlers[opt](this, options[opt], Init); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + })(); + } + + // DISPLAY CONSTRUCTOR + + function makeDisplay(place, docStart) { + var d = {}; + + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;"); + if (webkit) input.style.width = "1000px"; + else input.setAttribute("wrap", "off"); + // if border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) input.style.border = "1px solid black"; + input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); + + // Wraps and hides input textarea + d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The actual fake scrollbars. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar"); + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + // DIVs containing the selection and the actual code + d.lineDiv = elt("div", null, "CodeMirror-code"); + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + // Blinky cursor, and element used to ensure cursor fits at the end of a line + d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); + // Secondary cursor, shown when on a 'jump' in bi-directional text + d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"); + // Used to measure text size + d.measure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the text, causes scrolling + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + // D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); + // Will contain the gutters, if any + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Provides scrolling + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, + d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + // Work around IE7 z-index bug + if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper); + + // Needed to hide big blue blinking cursor on Mobile Safari + if (ios) input.style.width = "0px"; + if (!webkit) d.scroller.draggable = true; + // Needed to handle Tab key in KHTML + if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px"; + + // Current visible range (may be bigger than the view window). + d.viewOffset = d.lastSizeC = 0; + d.showingFrom = d.showingTo = docStart; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // See readInput and resetInput + d.prevInput = ""; + // Set to true when a non-horizontal-scrolling widget is added. As + // an optimization, widget aligning is skipped when d is false. + d.alignWidgets = false; + // Flag that indicates whether we currently expect input to appear + // (after some event like 'keypress' or 'input') and are polling + // intensively. + d.pollingFast = false; + // Self-resetting timeout for the poller + d.poll = new Delayed(); + + d.cachedCharWidth = d.cachedTextHeight = null; + d.measureLineCache = []; + d.measureLineCachePos = 0; + + // Tracks when resetInput has punted to just putting a short + // string instead of the (large) selection. + d.inaccurateSelection = false; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + return d; + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + cm.display.wrapper.className += " CodeMirror-wrap"; + cm.display.sizer.style.minWidth = ""; + } else { + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", ""); + computeMaxLength(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) + return 0; + else if (wrapping) + return (Math.ceil(line.text.length / perLine) || 1) * th; + else + return th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function keyMapChanged(cm) { + var map = keyMap[cm.options.keyMap], style = map.style; + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + + (style ? " cm-keymap-" + style : ""); + cm.state.disableInput = map.disableInput; + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + } + + function lineLength(doc, line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(); + cur = getLine(doc, found.from.line); + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(); + len -= cur.text.length - found.from.ch; + cur = getLine(doc, found.to.line); + len += cur.text.length - found.to.ch; + } + return len; + } + + function computeMaxLength(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(doc, d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(doc, line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Re-synchronize the fake scrollbars with the actual size of the + // content. Optionally force a scrollTop. + function updateScrollbars(cm) { + var d = cm.display, docHeight = cm.doc.height; + var totalHeight = docHeight + paddingVert(d); + d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; + d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px"; + var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); + var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1); + var needsV = scrollHeight > (d.scroller.clientHeight + 1); + if (needsV) { + d.scrollbarV.style.display = "block"; + d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarV.firstChild.style.height = + (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; + } else { + d.scrollbarV.style.display = ""; + d.scrollbarV.firstChild.style.height = "0"; + } + if (needsH) { + d.scrollbarH.style.display = "block"; + d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarH.firstChild.style.width = + (d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px"; + } else { + d.scrollbarH.style.display = ""; + d.scrollbarH.firstChild.style.width = "0"; + } + if (needsH && needsV) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px"; + } else d.scrollbarFiller.style.display = ""; + if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px"; + d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; + } else d.gutterFiller.style.display = ""; + + if (mac_geLion && scrollbarWidth(d.measure) === 0) { + d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; + d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none"; + } + } + + function visibleLines(display, doc, viewPort) { + var top = display.scroller.scrollTop, height = display.wrapper.clientHeight; + if (typeof viewPort == "number") top = viewPort; + else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;} + top = Math.floor(top - paddingTop(display)); + var bottom = Math.ceil(top + height); + return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; + } + + // LINE NUMBERS + + function alignHorizontally(cm) { + var display = cm.display; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, l = comp + "px"; + for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) { + for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + function compensateForHScroll(display) { + return getRect(display.scroller).left - getRect(display.sizer).left; + } + + // DISPLAY DRAWING + + function updateDisplay(cm, changes, viewPort, forced) { + var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + var visible = visibleLines(cm.display, cm.doc, viewPort); + for (var first = true;; first = false) { + var oldWidth = cm.display.scroller.clientWidth; + if (!updateDisplayInner(cm, changes, visible, forced)) break; + updated = true; + changes = []; + updateSelection(cm); + updateScrollbars(cm); + if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) { + forced = true; + continue; + } + forced = false; + + // Clip forced viewport to actual scrollable area + if (viewPort) + viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, + typeof viewPort == "number" ? viewPort : viewPort.top); + visible = visibleLines(cm.display, cm.doc, viewPort); + if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) + break; + } + + if (updated) { + signalLater(cm, "update", cm); + if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) + signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); + } + return updated; + } + + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplayInner(cm, changes, visible, forced) { + var display = cm.display, doc = cm.doc; + if (!display.wrapper.clientWidth) { + display.showingFrom = display.showingTo = doc.first; + display.viewOffset = 0; + return; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!forced && changes.length == 0 && + visible.from > display.showingFrom && visible.to < display.showingTo) + return; + + if (maybeUpdateLineNumberWidth(cm)) + changes = [{from: doc.first, to: doc.first + doc.size}]; + var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px"; + display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0"; + + // Used to determine which lines need their line numbers updated + var positionsChangedFrom = Infinity; + if (cm.options.lineNumbers) + for (var i = 0; i < changes.length; ++i) + if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; } + + var end = doc.first + doc.size; + var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, visible.to + cm.options.viewportMargin); + if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom); + if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo); + if (sawCollapsedSpans) { + from = lineNo(visualLine(doc, getLine(doc, from))); + while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to; + } + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = [{from: Math.max(display.showingFrom, doc.first), + to: Math.min(display.showingTo, end)}]; + if (intact[0].from >= intact[0].to) intact = []; + else intact = computeIntact(intact, changes); + // When merged lines are present, we might have to reduce the + // intact ranges because changes in continued fragments of the + // intact lines do require the lines to be redrawn. + if (sawCollapsedSpans) + for (var i = 0; i < intact.length; ++i) { + var range = intact[i], merged; + while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) { + var newTo = merged.find().from.line; + if (newTo > range.from) range.to = newTo; + else { intact.splice(i--, 1); break; } + } + } + + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) range.from = from; + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) { + updateViewOffset(cm); + return; + } + intact.sort(function(a, b) {return a.from - b.from;}); + + // Avoid crashing on IE's "unspecified error" when in iframes + try { + var focused = document.activeElement; + } catch(e) {} + if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none"; + patchDisplay(cm, from, to, intact, positionsChangedFrom); + display.lineDiv.style.display = ""; + if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus(); + + var different = from != display.showingFrom || to != display.showingTo || + display.lastSizeC != display.wrapper.clientHeight; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) { + display.lastSizeC = display.wrapper.clientHeight; + startWorker(cm, 400); + } + display.showingFrom = from; display.showingTo = to; + + updateHeightsInViewport(cm); + updateViewOffset(cm); + + return true; + } + + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { + if (ie_lt8) { + var bot = node.offsetTop + node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = getRect(node); + height = box.bottom - box.top; + } + var diff = node.lineObj.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(node.lineObj, height); + var widgets = node.lineObj.widgets; + if (widgets) for (var i = 0; i < widgets.length; ++i) + widgets[i].height = widgets[i].node.offsetHeight; + } + } + } + + function updateViewOffset(cm) { + var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom)); + // Position the mover div to align with the current virtual scroll position + cm.display.mover.style.top = off + "px"; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) { + intact2.push({from: range.from + diff, to: range.to + diff}); + } else if (change.to <= range.from || change.from >= range.to) { + intact2.push(range); + } else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff}); + } + } + intact = intact2; + } + return intact; + } + + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft; + width[cm.options.gutters[i]] = n.offsetWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + function patchDisplay(cm, from, to, intact, updateNumbersFrom) { + var dims = getDimensions(cm); + var display = cm.display, lineNumbers = cm.options.lineNumbers; + if (!intact.length && (!webkit || !cm.display.currentWheelTarget)) + removeChildren(display.lineDiv); + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + if (webkit && mac && cm.display.currentWheelTarget == node) { + node.style.display = "none"; + node.lineObj = null; + } else { + node.parentNode.removeChild(node); + } + return next; + } + + var nextIntact = intact.shift(), lineN = from; + cm.doc.iter(from, to, function(line) { + if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift(); + if (lineIsHidden(cm.doc, line)) { + if (line.height != 0) updateLineHeight(line, 0); + if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i]; + if (w.showIfHidden) { + var prev = cur.previousSibling; + if (/pre/i.test(prev.nodeName)) { + var wrap = elt("div", null, null, "position: relative"); + prev.parentNode.replaceChild(wrap, prev); + wrap.appendChild(prev); + prev = wrap; + } + var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget")); + if (!w.handleMouseEvents) wnode.ignoreEvents = true; + positionLineWidget(w, wnode, prev, dims); + } + } + } else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) { + // This line is intact. Skip to the actual node. Update its + // line number if needed. + while (cur.lineObj != line) cur = rm(cur); + if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber) + setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN)); + cur = cur.nextSibling; + } else { + // For lines with widgets, make an attempt to find and reuse + // the existing element, so that widgets aren't needlessly + // removed and re-inserted into the dom + if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling) + if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; } + // This line needs to be generated. + var lineNode = buildLineElement(cm, line, lineN, dims, reuse); + if (lineNode != reuse) { + container.insertBefore(lineNode, cur); + } else { + while (cur != reuse) cur = rm(cur); + cur = cur.nextSibling; + } + + lineNode.lineObj = line; + } + ++lineN; + }); + while (cur) cur = rm(cur); + } + + function buildLineElement(cm, line, lineNo, dims, reuse) { + var built = buildLineContent(cm, line), lineElement = built.pre; + var markers = line.gutterMarkers, display = cm.display, wrap; + + var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass; + if (!cm.options.lineNumbers && !markers && !bgClass && !line.wrapClass && !line.widgets) + return lineElement; + + // Lines with gutter elements, widgets or a background class need + // to be wrapped again, and have the extra elements added to the + // wrapper div + + if (reuse) { + reuse.alignable = null; + var isOk = true, widgetsSeen = 0, insertBefore = null; + for (var n = reuse.firstChild, next; n; n = next) { + next = n.nextSibling; + if (!/\bCodeMirror-linewidget\b/.test(n.className)) { + reuse.removeChild(n); + } else { + for (var i = 0; i < line.widgets.length; ++i) { + var widget = line.widgets[i]; + if (widget.node == n.firstChild) { + if (!widget.above && !insertBefore) insertBefore = n; + positionLineWidget(widget, n, reuse, dims); + ++widgetsSeen; + break; + } + } + if (i == line.widgets.length) { isOk = false; break; } + } + } + reuse.insertBefore(lineElement, insertBefore); + if (isOk && widgetsSeen == line.widgets.length) { + wrap = reuse; + reuse.className = line.wrapClass || ""; + } + } + if (!wrap) { + wrap = elt("div", null, line.wrapClass, "position: relative"); + wrap.appendChild(lineElement); + } + // Kludge to make sure the styled element lies behind the selection (by z-index) + if (bgClass) + wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild); + if (cm.options.lineNumbers || markers) { + var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + wrap.firstChild); + if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap); + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + wrap.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineNo), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + display.lineNumInnerWidth + "px")); + if (markers) + for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + if (ie_lt8) wrap.style.zIndex = 2; + if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.ignoreEvents = true; + positionLineWidget(widget, node, wrap, dims); + if (widget.above) + wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + return wrap; + } + + function positionLineWidget(widget, node, wrap, dims) { + if (widget.noHScroll) { + (wrap.alignable || (wrap.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // SELECTION / CURSOR + + function updateSelection(cm) { + var display = cm.display; + var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to); + if (collapsed || cm.options.showCursorWhenSelecting) + updateSelectionCursor(cm); + else + display.cursor.style.display = display.otherCursor.style.display = "none"; + if (!collapsed) + updateSelectionRange(cm); + else + display.selectionDiv.style.display = "none"; + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, cm.doc.sel.head, "div"); + var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv); + display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)) + "px"; + display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)) + "px"; + } + } + + // No selection, plain cursor + function updateSelectionCursor(cm) { + var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div"); + display.cursor.style.left = pos.left + "px"; + display.cursor.style.top = pos.top + "px"; + display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + display.cursor.style.display = ""; + + if (pos.other) { + display.otherCursor.style.display = ""; + display.otherCursor.style.left = pos.other.left + "px"; + display.otherCursor.style.top = pos.other.top + "px"; + display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } else { display.otherCursor.style.display = "none"; } + } + + // Highlight selection + function updateSelectionRange(cm) { + var display = cm.display, doc = cm.doc, sel = cm.doc.sel; + var fragment = document.createDocumentFragment(); + var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display); + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = pl; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = pl; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = clientWidth; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < pl + 1) left = pl; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + if (sel.from.line == sel.to.line) { + drawForLine(sel.from.line, sel.from.ch, sel.to.ch); + } else { + var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line); + var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine); + var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end; + var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(pl, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(pl, leftEnd.bottom, null, rightStart.top); + } + + removeChildrenAndAdd(display.selectionDiv, fragment); + display.selectionDiv.style.display = ""; + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursor.style.visibility = display.otherCursor.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.showingTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changed = [], prevChange; + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { + if (doc.frontier >= cm.display.showingFrom) { // Visible + var oldStyles = line.styles; + line.styles = highlightLine(cm, line, state); + var ischange = !oldStyles || oldStyles.length != line.styles.length; + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) { + if (prevChange && prevChange.end == doc.frontier) prevChange.end++; + else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1}); + } + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changed.length) + operation(cm, function() { + for (var i = 0; i < changed.length; ++i) + regChange(this, changed[i].start, changed[i].end); + })(); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingLeft(display) { + var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x")); + return e.offsetLeft; + } + + function measureChar(cm, line, ch, data, bias) { + var dir = -1; + data = data || measureLine(cm, line); + if (data.crude) { + var left = data.left + ch * data.width; + return {left: left, right: left + data.width, top: data.top, bottom: data.bottom}; + } + + for (var pos = ch;; pos += dir) { + var r = data[pos]; + if (r) break; + if (dir < 0 && pos == 0) dir = 1; + } + bias = pos > ch ? "left" : pos < ch ? "right" : bias; + if (bias == "left" && r.leftSide) r = r.leftSide; + else if (bias == "right" && r.rightSide) r = r.rightSide; + return {left: pos < ch ? r.right : r.left, + right: pos > ch ? r.left : r.right, + top: r.top, + bottom: r.bottom}; + } + + function findCachedMeasurement(cm, line) { + var cache = cm.display.measureLineCache; + for (var i = 0; i < cache.length; ++i) { + var memo = cache[i]; + if (memo.text == line.text && memo.markedSpans == line.markedSpans && + cm.display.scroller.clientWidth == memo.width && + memo.classes == line.textClass + "|" + line.wrapClass) + return memo; + } + } + + function clearCachedMeasurement(cm, line) { + var exists = findCachedMeasurement(cm, line); + if (exists) exists.text = exists.measure = exists.markedSpans = null; + } + + function measureLine(cm, line) { + // First look in the cache + var cached = findCachedMeasurement(cm, line); + if (cached) return cached.measure; + + // Failing that, recompute and store result in cache + var measure = measureLineInner(cm, line); + var cache = cm.display.measureLineCache; + var memo = {text: line.text, width: cm.display.scroller.clientWidth, + markedSpans: line.markedSpans, measure: measure, + classes: line.textClass + "|" + line.wrapClass}; + if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; + else cache.push(memo); + return measure; + } + + function measureLineInner(cm, line) { + if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom) + return crudelyMeasureLine(cm, line); + + var display = cm.display, measure = emptyArray(line.text.length); + var pre = buildLineContent(cm, line, measure, true).pre; + + // IE does not cache element positions of inline elements between + // calls to getBoundingClientRect. This makes the loop below, + // which gathers the positions of all the characters on the line, + // do an amount of layout work quadratic to the number of + // characters. When line wrapping is off, we try to improve things + // by first subdividing the line into a bunch of inline blocks, so + // that IE can reuse most of the layout information from caches + // for those blocks. This does interfere with line wrapping, so it + // doesn't work when wrapping is on, but in that case the + // situation is slightly better, since IE does cache line-wrapping + // information and only recomputes per-line. + if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) { + var fragment = document.createDocumentFragment(); + var chunk = 10, n = pre.childNodes.length; + for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) { + var wrap = elt("div", null, null, "display: inline-block"); + for (var j = 0; j < chunk && n; ++j) { + wrap.appendChild(pre.firstChild); + --n; + } + fragment.appendChild(wrap); + } + pre.appendChild(fragment); + } + + removeChildrenAndAdd(display.measure, pre); + + var outer = getRect(display.lineDiv); + var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight; + // Work around an IE7/8 bug where it will sometimes have randomly + // replaced our pre with a clone at this point. + if (ie_lt9 && display.measure.first != pre) + removeChildrenAndAdd(display.measure, pre); + + function measureRect(rect) { + var top = rect.top - outer.top, bot = rect.bottom - outer.top; + if (bot > maxBot) bot = maxBot; + if (top < 0) top = 0; + for (var i = vranges.length - 2; i >= 0; i -= 2) { + var rtop = vranges[i], rbot = vranges[i+1]; + if (rtop > bot || rbot < top) continue; + if (rtop <= top && rbot >= bot || + top <= rtop && bot >= rbot || + Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) { + vranges[i] = Math.min(top, rtop); + vranges[i+1] = Math.max(bot, rbot); + break; + } + } + if (i < 0) { i = vranges.length; vranges.push(top, bot); } + return {left: rect.left - outer.left, + right: rect.right - outer.left, + top: i, bottom: null}; + } + function finishRect(rect) { + rect.bottom = vranges[rect.top+1]; + rect.top = vranges[rect.top]; + } + + for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { + var node = cur, rect = null; + // A widget might wrap, needs special care + if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) { + if (cur.firstChild.nodeType == 1) node = cur.firstChild; + var rects = node.getClientRects(); + if (rects.length > 1) { + rect = data[i] = measureRect(rects[0]); + rect.rightSide = measureRect(rects[rects.length - 1]); + } + } + if (!rect) rect = data[i] = measureRect(getRect(node)); + if (cur.measureRight) rect.right = getRect(cur.measureRight).left; + if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide)); + } + removeChildren(cm.display.measure); + for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { + finishRect(cur); + if (cur.leftSide) finishRect(cur.leftSide); + if (cur.rightSide) finishRect(cur.rightSide); + } + return data; + } + + function crudelyMeasureLine(cm, line) { + var copy = new Line(line.text.slice(0, 100), null); + if (line.textClass) copy.textClass = line.textClass; + var measure = measureLineInner(cm, copy); + var left = measureChar(cm, copy, 0, measure, "left"); + var right = measureChar(cm, copy, 99, measure, "right"); + return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100}; + } + + function measureLineWidth(cm, line) { + var hasBadSpan = false; + if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) { + var sp = line.markedSpans[i]; + if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true; + } + var cached = !hasBadSpan && findCachedMeasurement(cm, line); + if (cached || line.text.length >= cm.options.crudeMeasuringFrom) + return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right; + + var pre = buildLineContent(cm, line, null, true).pre; + var end = pre.appendChild(zeroWidthElement(cm.display.measure)); + removeChildrenAndAdd(cm.display.measure, pre); + return getRect(end).right - getRect(cm.display.lineDiv).left; + } + + function clearCaches(cm) { + cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0; + cm.display.cachedCharWidth = cm.display.cachedTextHeight = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page" + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(cm, lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = getRect(cm.display.lineSpace); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Context may be "window", "page", "div", or "local"/null + // Result is in "div" coords + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = getRect(cm.display.sizer); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = getRect(cm.display.lineSpace); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), context); + } + + function cursorCoords(cm, pos, context, lineObj, measurement) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!measurement) measurement = measureLine(cm, lineObj); + function get(ch, right) { + var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left"); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + function PosWithInfo(line, ch, outside, xRel) { + var pos = new Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Coords must be lineSpace-local + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineNo > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + for (;;) { + var lineObj = getLine(doc, lineNo); + var found = coordsCharInner(cm, lineObj, lineNo, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineNo = mergedPos.to.line; + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(cm, lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var measurement = measureLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", + lineObj, measurement); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < 0 ? -1 : xDiff ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "x"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var width = anchor.offsetWidth; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + + var nextOpId = 0; + function startOperation(cm) { + cm.curOp = { + // An array of ranges of lines that have to be updated. See + // updateDisplay. + changes: [], + forceUpdate: false, + updateInput: null, + userSelChange: null, + textChanged: null, + selectionChanged: false, + cursorActivity: false, + updateMaxLine: false, + updateScrollPos: false, + id: ++nextOpId + }; + if (!delayedCallbackDepth++) delayedCallbacks = []; + } + + function endOperation(cm) { + var op = cm.curOp, doc = cm.doc, display = cm.display; + cm.curOp = null; + + if (op.updateMaxLine) computeMaxLength(cm); + if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) { + var width = measureLineWidth(cm, display.maxLine); + display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px"; + display.maxLineChanged = false; + var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth); + if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos) + setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + } + var newScrollPos, updated; + if (op.updateScrollPos) { + newScrollPos = op.updateScrollPos; + } else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible + var coords = cursorCoords(cm, doc.sel.head); + newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom); + } + if (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) { + updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate); + if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop; + } + if (!updated && op.selectionChanged) updateSelection(cm); + if (op.updateScrollPos) { + display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = newScrollPos.scrollTop; + display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = newScrollPos.scrollLeft; + alignHorizontally(cm); + if (op.scrollToPos) + scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), + clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); + } else if (newScrollPos) { + scrollCursorIntoView(cm); + } + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.userSelChange); + + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + var delayed; + if (!--delayedCallbackDepth) { + delayed = delayedCallbacks; + delayedCallbacks = null; + } + if (op.textChanged) + signal(cm, "change", cm, op.textChanged); + if (op.cursorActivity) signal(cm, "cursorActivity", cm); + if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm1, f) { + return function() { + var cm = cm1 || this, withOp = !cm.curOp; + if (withOp) startOperation(cm); + try { var result = f.apply(cm, arguments); } + finally { if (withOp) endOperation(cm); } + return result; + }; + } + function docOperation(f) { + return function() { + var withOp = this.cm && !this.cm.curOp, result; + if (withOp) startOperation(this.cm); + try { result = f.apply(this, arguments); } + finally { if (withOp) endOperation(this.cm); } + return result; + }; + } + function runInOp(cm, f) { + var withOp = !cm.curOp, result; + if (withOp) startOperation(cm); + try { result = f(); } + finally { if (withOp) endOperation(cm); } + return result; + } + + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + cm.curOp.changes.push({from: from, to: to, diff: lendiff}); + } + + // INPUT HANDLING + + function slowPoll(cm) { + if (cm.display.pollingFast) return; + cm.display.poll.set(cm.options.pollInterval, function() { + readInput(cm); + if (cm.state.focused) slowPoll(cm); + }); + } + + function fastPoll(cm) { + var missed = false; + cm.display.pollingFast = true; + function p() { + var changed = readInput(cm); + if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} + else {cm.display.pollingFast = false; slowPoll(cm);} + } + cm.display.poll.set(20, p); + } + + // prevInput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + function readInput(cm) { + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel; + if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.state.disableInput) return false; + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + if (text == prevInput && posEq(sel.from, sel.to)) return false; + if (ie && !ie_lt9 && cm.display.inputHasSelection === text) { + resetInput(cm, true); + return false; + } + + var withOp = !cm.curOp; + if (withOp) startOperation(cm); + sel.shift = false; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + var from = sel.from, to = sel.to; + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same))); + + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: splitLines(text.slice(same)), + origin: cm.state.pasteIncoming ? "paste" : "+input"}; + makeChange(cm.doc, changeEvent, "end"); + cm.curOp.updateInput = updateInput; + signalLater(cm, "inputRead", cm, changeEvent); + + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; + else cm.display.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = false; + return true; + } + + function resetInput(cm, user) { + var minimal, selected, doc = cm.doc; + if (!posEq(doc.sel.from, doc.sel.to)) { + cm.display.prevInput = ""; + minimal = hasCopyEvent && + (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; + if (cm.state.focused) selectInput(cm.display.input); + if (ie && !ie_lt9) cm.display.inputHasSelection = content; + } else if (user) { + cm.display.prevInput = cm.display.input.value = ""; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + } + cm.display.inaccurateSelection = minimal; + } + + function focusInput(cm) { + if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input)) + cm.display.input.focus(); + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // EVENT HANDLERS + + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + if (ie) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = findWordAt(getLine(cm.doc, pos.line).text, pos); + extendSelection(cm.doc, word.from, word.to); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + on(d.lineSpace, "selectstart", function(e) { + if (!eventInWidget(d, e)) e_preventDefault(e); + }); + // Gecko browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for Gecko. + if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + on(d.scrollbarV, "scroll", function() { + if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); + }); + on(d.scrollbarH, "scroll", function() { + if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); + }); + + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } + on(d.scrollbarH, "mousedown", reFocus); + on(d.scrollbarV, "mousedown", reFocus); + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + var resizeTimer; + function onResize() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null; + clearCaches(cm); + runInOp(cm, bind(regChange, cm)); + }, 100); + } + on(window, "resize", onResize); + // Above handler holds on to the editor and its data structures. + // Here we poll to unregister it when the editor is no longer in + // the document, so that it can be garbage-collected. + function unregister() { + for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {} + if (p) setTimeout(unregister, 5000); + else off(window, "resize", onResize); + } + setTimeout(unregister, 5000); + + on(d.input, "keyup", operation(cm, function(e) { + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (e.keyCode == 16) cm.doc.sel.shift = false; + })); + on(d.input, "input", function() { + if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + fastPoll(cm); + }); + on(d.input, "keydown", operation(cm, onKeyDown)); + on(d.input, "keypress", operation(cm, onKeyPress)); + on(d.input, "focus", bind(onFocus, cm)); + on(d.input, "blur", bind(onBlur, cm)); + + function drag_(e) { + if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; + e_stop(e); + } + if (cm.options.dragDrop) { + on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); + on(d.scroller, "dragenter", drag_); + on(d.scroller, "dragover", drag_); + on(d.scroller, "drop", operation(cm, onDrop)); + } + on(d.scroller, "paste", function(e) { + if (eventInWidget(d, e)) return; + focusInput(cm); + fastPoll(cm); + }); + on(d.input, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = d.input.selectionStart, end = d.input.selectionEnd; + d.input.value += "$"; + d.input.selectionStart = start; + d.input.selectionEnd = end; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + fastPoll(cm); + }); + + function prepareCopy() { + if (d.inaccurateSelection) { + d.prevInput = ""; + d.inaccurateSelection = false; + d.input.value = cm.getSelection(); + selectInput(d.input); + } + } + on(d.input, "cut", prepareCopy); + on(d.input, "copy", prepareCopy); + + // Needed to handle Tab key in KHTML + if (khtml) on(d.sizer, "mouseup", function() { + if (document.activeElement == d.input) d.input.blur(); + focusInput(cm); + }); + } + + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true; + } + } + + function posFromMouse(cm, e, liberal) { + var display = cm.display; + if (!liberal) { + var target = e_target(e); + if (target == display.scrollbarH || target == display.scrollbarH.firstChild || + target == display.scrollbarV || target == display.scrollbarV.firstChild || + target == display.scrollbarFiller || target == display.gutterFiller) return null; + } + var x, y, space = getRect(display.lineSpace); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + return coordsChar(cm, x - space.left, y - space.top); + } + + var lastClick, lastDoubleClick; + function onMouseDown(e) { + if (signalDOMEvent(this, e)) return; + var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; + sel.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + + switch (e_button(e)) { + case 3: + if (captureMiddleClick) onContextMenu.call(cm, cm, e); + return; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(bind(focusInput, cm), 20); + e_preventDefault(e); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;} + + if (!cm.state.focused) onFocus(cm); + + var now = +new Date, type = "single"; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + type = "triple"; + e_preventDefault(e); + setTimeout(bind(focusInput, cm), 20); + selectLine(cm, start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + var word = findWordAt(getLine(doc, start.line).text, start); + extendSelection(cm.doc, word.from, word.to); + } else { lastClick = {time: now, pos: start}; } + + var last = start; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") { + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + extendSelection(cm.doc, start); + focusInput(cm); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + return; + } + e_preventDefault(e); + if (type == "single") extendSelection(cm.doc, clipPos(doc, start)); + + var startstart = sel.from, startend = sel.to, lastPos = start; + + function doSelect(cur) { + if (posEq(lastPos, cur)) return; + lastPos = cur; + + if (type == "single") { + extendSelection(cm.doc, clipPos(doc, start), cur); + return; + } + + startstart = clipPos(doc, startstart); + startend = clipPos(doc, startend); + if (type == "double") { + var word = findWordAt(getLine(doc, cur.line).text, cur); + if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend); + else extendSelection(cm.doc, startstart, word.to); + } else if (type == "triple") { + if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0))); + else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0))); + } + } + + var editorSize = getRect(display.wrapper); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true); + if (!cur) return; + if (!posEq(cur, last)) { + if (!cm.state.focused) onFocus(cm); + last = cur; + doSelect(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + focusInput(cm); + off(document, "mousemove", move); + off(document, "mouseup", up); + } + + var move = operation(cm, function(e) { + if (!ie && !e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(getRect(cm.display.gutters).right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = getRect(display.lineDiv); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && getRect(g).right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around"); + } + }; + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(bind(focusInput, cm), 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to; + setSelection(cm.doc, pos, pos); + if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste"); + cm.replaceSelection(text, null, "paste"); + focusInput(cm); + onFocus(cm); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + var txt = cm.getSelection(); + e.dataTransfer.setData("Text", txt); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (opera) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (opera) img.parentNode.removeChild(img); + } + } + + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplay(cm, [], val); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; + if (gecko) updateDisplay(cm, []); + startWorker(cm, 100); + } + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val; + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + function onScrollWheel(cm, e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + for (var cur = e.target; cur != scroll; cur = cur.parentNode) { + if (cur.lineObj) { + cm.display.currentWheelTarget = cur; + break; + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !opera && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplay(cm, [], {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; + var doc = cm.doc, prevShift = doc.sel.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) doc.sel.shift = false; + done = bound(cm) != Pass; + } finally { + doc.sel.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function allKeyMaps(cm) { + var maps = cm.state.keyMaps.slice(0); + if (cm.options.extraKeys) maps.push(cm.options.extraKeys); + maps.push(cm.options.keyMap); + return maps; + } + + var maybeTransition; + function handleKeyBinding(cm, e) { + // Handle auto keymap transitions + var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; + clearTimeout(maybeTransition); + if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { + if (getKeyMap(cm.options.keyMap) == startMap) { + cm.options.keyMap = (next.call ? next.call(null, cm) : next); + keyMapChanged(cm); + } + }, 50); + + var name = keyName(e, true), handled = false; + if (!name) return false; + var keymaps = allKeyMaps(cm); + + if (e.shiftKey) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);}) + || lookupKey(name, keymaps, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); }); + } + + if (handled) { + e_preventDefault(e); + restartBlink(cm); + if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } + signalLater(cm, "keyHandled", cm, name, e); + } + return handled; + } + + function handleCharBinding(cm, e, ch) { + var handled = lookupKey("'" + ch + "'", allKeyMaps(cm), + function(b) { return doHandleBinding(cm, b, true); }); + if (handled) { + e_preventDefault(e); + restartBlink(cm); + signalLater(cm, "keyHandled", cm, "'" + ch + "'", e); + } + return handled; + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (!cm.state.focused) onFocus(cm); + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (ie && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + // IE does strange things with escape. + cm.doc.sel.shift = code == 16 || e.shiftKey; + // First give onKeyEvent option a chance to handle this. + var handled = handleKeyBinding(cm, e); + if (opera) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection(""); + } + } + + function onKeyPress(e) { + var cm = this; + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (this.options.electricChars && this.doc.mode.electricChars && + this.options.smartIndent && !isReadOnly(this) && + this.doc.mode.electricChars.indexOf(ch) > -1) + setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75); + if (handleCharBinding(cm, e, ch)) return; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + fastPoll(cm); + } + + function onFocus(cm) { + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + cm.display.wrapper.className += " CodeMirror-focused"; + if (!cm.curOp) { + resetInput(cm, true); + if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 + } + } + slowPoll(cm); + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150); + } + + var detectingSelectAll; + function onContextMenu(cm, e) { + if (signalDOMEvent(cm, e, "contextmenu")) return; + var display = cm.display, sel = cm.doc.sel; + if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; + + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || opera) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))) + operation(cm, setSelection)(cm.doc, pos, pos); + + var oldCSS = display.input.style.cssText; + display.inputDiv.style.position = "absolute"; + display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);"; + focusInput(cm); + resetInput(cm, true); + // Adds "Select all" to context menu in FF + if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; + + function prepareSelectAllHack() { + if (display.input.selectionStart != null) { + var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value); + display.prevInput = "\u200b"; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + } + } + function rehide() { + display.inputDiv.style.position = "relative"; + display.input.style.cssText = oldCSS; + if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + slowPoll(cm); + + // Try to detect the user choosing select-all + if (display.input.selectionStart != null) { + if (!ie || ie_lt9) prepareSelectAllHack(); + clearTimeout(detectingSelectAll); + var i = 0, poll = function(){ + if (display.prevInput == " " && display.input.selectionStart == 0) + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); + else resetInput(cm); + }; + detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && !ie_lt9) prepareSelectAllHack(); + if (captureMiddleClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + } + + // UPDATING + + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Make sure a position will be valid after the given change. + function clipPostChange(doc, change, pos) { + if (!posLess(change.from, pos)) return clipPos(doc, pos); + var diff = (change.text.length - 1) - (change.to.line - change.from.line); + if (pos.line > change.to.line + diff) { + var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1; + if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length); + return clipToLen(pos, getLine(doc, preLine).text.length); + } + if (pos.line == change.to.line + diff) + return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) + + getLine(doc, change.to.line).text.length - change.to.ch); + var inside = pos.line - change.from.line; + return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch)); + } + + // Hint can be null|"end"|"start"|"around"|{anchor,head} + function computeSelAfterChange(doc, change, hint) { + if (hint && typeof hint == "object") // Assumed to be {anchor, head} object + return {anchor: clipPostChange(doc, change, hint.anchor), + head: clipPostChange(doc, change, hint.head)}; + + if (hint == "start") return {anchor: change.from, head: change.from}; + + var end = changeEnd(change); + if (hint == "around") return {anchor: change.from, head: end}; + if (hint == "end") return {anchor: end, head: end}; + + // hint is null, leave the selection alone as much as possible + var adjustPos = function(pos) { + if (posLess(pos, change.from)) return pos; + if (!posLess(change.to, pos)) return end; + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += end.ch - change.to.ch; + return Pos(line, ch); + }; + return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)}; + } + + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Replace the range from from to to by the strings in replacement. + // change is a {from, to, text [, origin]} object + function makeChange(doc, change, selUpdate, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 1; --i) + makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]}); + if (split.length) + makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate); + } else { + makeChangeNoReadonly(doc, change, selUpdate); + } + } + + function makeChangeNoReadonly(doc, change, selUpdate) { + if (change.text.length == 1 && change.text[0] == "" && posEq(change.from, change.to)) return; + var selAfter = computeSelAfterChange(doc, change, selUpdate); + addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + function makeChangeFromHistory(doc, type) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history; + var event = (type == "undo" ? hist.done : hist.undone).pop(); + if (!event) return; + + var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter, + anchorAfter: event.anchorBefore, headAfter: event.headBefore, + generation: hist.generation}; + (type == "undo" ? hist.undone : hist.done).push(anti); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + (type == "undo" ? hist.done : hist.undone).length = 0; + return; + } + + anti.changes.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change, null) + : {anchor: event.anchorBefore, head: event.headBefore}; + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + function shiftDoc(doc, distance) { + function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);} + doc.first += distance; + if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance); + doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor); + doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to); + } + + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change, null); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter); + else updateDoc(doc, change, spans, selAfter); + } + + function makeChangeSingleDocInEditor(cm, change, spans, selAfter) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head)) + cm.curOp.cursorActivity = true; + + updateDoc(doc, change, spans, selAfter, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(doc, line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + regChange(cm, from.line, to.line + 1, lendiff); + + if (hasHandler(cm, "change")) { + var changeObj = {from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin}; + if (cm.curOp.textChanged) { + for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else cm.curOp.textChanged = changeObj; + } + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (posLess(to, from)) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}, null); + } + + // POSITION OBJECT + + function Pos(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + } + CodeMirror.Pos = Pos; + + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return Pos(x.line, x.ch);} + + // SELECTION + + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + + // If shift is held, this will move the selection anchor. Otherwise, + // it'll set the whole selection. + function extendSelection(doc, pos, other, bias) { + if (doc.sel.shift || doc.sel.extend) { + var anchor = doc.sel.anchor; + if (other) { + var posBefore = posLess(pos, anchor); + if (posBefore != posLess(other, anchor)) { + anchor = pos; + pos = other; + } else if (posBefore != posLess(pos, other)) { + pos = other; + } + } + setSelection(doc, anchor, pos, bias); + } else { + setSelection(doc, pos, other || pos, bias); + } + if (doc.cm) doc.cm.curOp.userSelChange = true; + } + + function filterSelectionChange(doc, anchor, head) { + var obj = {anchor: anchor, head: head}; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head); + return obj; + } + + // Update the selection. Last two args are only used by + // updateDoc, since they have to be expressed in the line + // numbers before the update. + function setSelection(doc, anchor, head, bias, checkAtomic) { + if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) { + var filtered = filterSelectionChange(doc, anchor, head); + head = filtered.head; + anchor = filtered.anchor; + } + + var sel = doc.sel; + sel.goalColumn = null; + if (bias == null) bias = posLess(head, sel.head) ? -1 : 1; + // Skip over atomic spans. + if (checkAtomic || !posEq(anchor, sel.anchor)) + anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push"); + if (checkAtomic || !posEq(head, sel.head)) + head = skipAtomic(doc, head, bias, checkAtomic != "push"); + + if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return; + + sel.anchor = anchor; sel.head = head; + var inv = posLess(head, anchor); + sel.from = inv ? head : anchor; + sel.to = inv ? anchor : head; + + if (doc.cm) + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; + + signalLater(doc, "cursorActivity", doc); + } + + function reCheckSelection(cm) { + setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push"); + } + + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find()[dir < 0 ? "from" : "to"]; + if (posEq(newPos, curPos)) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SCROLLING + + function scrollCursorIntoView(cm) { + var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin); + if (!cm.state.focused) return; + var display = cm.display, box = getRect(display.sizer), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var hidden = display.cursor.style.display == "none"; + if (hidden) { + display.cursor.style.display = ""; + display.cursor.style.left = coords.left + "px"; + display.cursor.style.top = (coords.top - display.viewOffset) + "px"; + } + display.cursor.scrollIntoView(doScroll); + if (hidden) display.cursor.style.display = "none"; + } + } + + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (;;) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) return coords; + } + } + + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {}; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft; + x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth; + var gutterw = display.gutters.offsetWidth; + var atLeft = x1 < gutterw + 10; + if (x1 < screenleft + gutterw || atLeft) { + if (atLeft) x1 = 0; + result.scrollLeft = Math.max(0, x1 - 10 - gutterw); + } else if (x2 > screenw + screenleft - 3) { + result.scrollLeft = x2 + 10 - screenw; + } + return result; + } + + function updateScrollPos(cm, left, top) { + cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left, + scrollTop: top == null ? cm.doc.scrollTop : top}; + } + + function addToScrollPos(cm, left, top) { + var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop}); + var scroll = cm.display.scroller; + pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top)); + pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left)); + } + + // API UTILITIES + + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc; + if (how == null) how = "add"; + if (how == "smart") { + if (!cm.doc.mode.indent) how = "prev"; + else var state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "smart") { + indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) + replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + } + + function changeLine(cm, handle, op) { + var no = handle, line = handle, doc = cm.doc; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) regChange(cm, no, no + 1); + else return null; + return line; + } + + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur) ? "w" + : !group ? null + : /\s/.test(cur) ? null + : "p"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + function findWordAt(line, pos) { + var start = pos.ch, end = pos.ch; + if (line) { + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar) ? isWordChar + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return {from: Pos(pos.line, start), to: Pos(pos.line, end)}; + } + + function selectLine(cm, line) { + extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0))); + } + + // PROTOTYPE + + // The publicly visible API. Note that operation(null, f) means + // 'wrap f in an operation, performed on its `this` parameter' + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](map); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: operation(null, function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: operation(null, function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: operation(null, function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: operation(null, function(how) { + var sel = this.doc.sel; + if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how); + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + var doc = this.doc; + pos = clipPos(doc, pos); + var state = getStateBefore(this, pos.line, precise), mode = this.doc.mode; + var line = getLine(doc, pos.line); + var stream = new StringStream(line.text, this.options.tabSize); + while (stream.pos < pos.ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, // Deprecated, use 'type' instead + type: style || null, + state: state}; + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + if (ch == 0) return styles[2]; + for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else return styles[mid * 2 + 2]; + } + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + if (!helpers.hasOwnProperty(type)) return; + var help = helpers[type], mode = this.getModeAt(pos); + return mode[type] && help[mode[type]] || + mode.helperType && help[mode.helperType] || + help[mode.name]; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, sel = this.doc.sel; + if (start == null) pos = sel.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? sel.from : sel.to; + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + var lineObj = getLine(this.doc, line); + return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top + + (end ? lineObj.height : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: operation(null, function(line, gutterID, value) { + return changeLine(this, line, function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: operation(null, function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regChange(cm, i, i + 1); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + addLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + + removeLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)")); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: operation(null, function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + + removeLineWidget: function(widget) { widget.clear(); }, + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: operation(null, onKeyDown), + + execCommand: function(cmd) {return commands[cmd](this);}, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: operation(null, function(dir, unit) { + var sel = this.doc.sel, pos; + if (sel.shift || sel.extend || posEq(sel.from, sel.to)) + pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually); + else + pos = dir < 0 ? sel.from : sel.to; + extendSelection(this.doc, pos, pos, dir); + }), + + deleteH: operation(null, function(dir, unit) { + var sel = this.doc.sel; + if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete"); + else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete"); + this.curOp.userSelChange = true; + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: operation(null, function(dir, unit) { + var sel = this.doc.sel; + var pos = cursorCoords(this, sel.head, "div"); + if (sel.goalColumn != null) pos.left = sel.goalColumn; + var target = findPosV(this, pos, dir, unit); + + if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top); + extendSelection(this.doc, target, target, dir); + sel.goalColumn = pos.left; + }), + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + this.display.cursor.className += " CodeMirror-overwrite"; + else + this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", ""); + }, + hasFocus: function() { return this.state.focused; }, + + scrollTo: operation(null, function(x, y) { + updateScrollPos(this, x, y); + }), + getScrollInfo: function() { + var scroller = this.display.scroller, co = scrollerCutOff; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - co, width: scroller.scrollWidth - co, + clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; + }, + + scrollIntoView: operation(null, function(range, margin) { + if (range == null) range = {from: this.doc.sel.head, to: null}; + else if (typeof range == "number") range = {from: Pos(range, 0), to: null}; + else if (range.from == null) range = {from: range, to: null}; + if (!range.to) range.to = range.from; + if (!margin) margin = 0; + + var coords = range; + if (range.from.line != null) { + this.curOp.scrollToPos = {from: range.from, to: range.to, margin: margin}; + coords = {from: cursorCoords(this, range.from), + to: cursorCoords(this, range.to)}; + } + var sPos = calculateScrollPos(this, Math.min(coords.from.left, coords.to.left), + Math.min(coords.from.top, coords.to.top) - margin, + Math.max(coords.from.right, coords.to.right), + Math.max(coords.from.bottom, coords.to.bottom) + margin); + updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop); + }), + + setSize: operation(null, function(width, height) { + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) this.display.wrapper.style.width = interpret(width); + if (height != null) this.display.wrapper.style.height = interpret(height); + if (this.options.lineWrapping) + this.display.measureLineCache.length = this.display.measureLineCachePos = 0; + this.curOp.forceUpdate = true; + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: operation(null, function() { + var badHeight = this.display.cachedTextHeight == null; + clearCaches(this); + updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); + regChange(this); + if (badHeight) estimateLineHeights(this); + }), + + swapDoc: operation(null, function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + resetInput(this, true); + updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input;}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + var optionHandlers = CodeMirror.optionHandlers = {}; + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + loadMode(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("electricChars", true); + option("rtlMoveVisually", !windows); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", keyMapChanged); + option("extraKeys", null); + + option("onKeyEvent", null); + option("onDragEvent", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, updateScrollbars, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") {onBlur(cm); cm.display.input.blur();} + else if (!val) resetInput(cm, true); + }); + option("dragDrop", true); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true); + option("pollInterval", 100); + option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 500); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, function(cm){loadMode(cm); cm.refresh();}, true); + option("crudeMeasuringFrom", 10000); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) { + mode.dependencies = []; + for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]); + } + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + + return modeObj; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {}; + helpers[type][name] = value; + }; + + // UTILITIES + + CodeMirror.isWordChar = isWordChar; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) + cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete"); + else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete"); + }, + deleteLine: function(cm) { + var l = cm.getCursor().line; + cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); + }, + delLineLeft: function(cm) { + var cur = cm.getCursor(); + cm.replaceRange("", Pos(cur.line, 0), cur, "+delete"); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelection(lineStart(cm, cm.getCursor().line)); + }, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(), start = lineStart(cm, cur.line); + var line = cm.getLineHandle(start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch; + cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS)); + } else cm.extendSelection(start); + }, + goLineEnd: function(cm) { + cm.extendSelection(lineEnd(cm, cm.getCursor().line)); + }, + goLineRight: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")); + }, + goLineLeft: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div")); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");}, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.replaceSelection("\t", "end", "+input"); + }, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + }, + newlineAndIndent: function(cm) { + operation(cm, function() { + cm.replaceSelection("\n", "end", "+input"); + cm.indentLine(cm.getCursor().line, null, true); + })(); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + // KEYMAP DISPATCH + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + + function lookupKey(name, maps, handle) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found === false) return "stop"; + if (found != null && handle(found)) return true; + if (map.nofallthrough) return "stop"; + + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0, e = fallthrough.length; i < e; ++i) { + var done = lookup(fallthrough[i]); + if (done) return done; + } + return false; + } + + for (var i = 0; i < maps.length; ++i) { + var done = lookup(maps[i]); + if (done) return done != "stop"; + } + } + function isModifierKey(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + function keyName(event, noShift) { + if (opera && event.keyCode == 34 && event["char"]) return false; + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) return false; + if (event.altKey) name = "Alt-" + name; + if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name; + if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name; + if (!noShift && event.shiftKey) name = "Shift-" + name; + return name; + } + CodeMirror.lookupKey = lookupKey; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.keyName = keyName; + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = document.body; + // doc.activeElement occasionally throws on IE + try { hasFocus = document.activeElement; } catch(e) {} + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + } + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue; + }, + indentation: function() {return countColumn(this.string, null, this.tabSize);}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);} + }; + CodeMirror.StringStream = StringStream; + + // TEXTMARKERS + + function TextMarker(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + } + CodeMirror.TextMarker = TextMarker; + eventMixin(TextMarker); + + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.to != null) max = lineNo(line); + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from != null) + min = lineNo(line); + else if (this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm); + } + if (withOp) endOperation(cm); + }; + + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null || span.to != null) { + var found = lineNo(line); + if (span.from != null) from = Pos(found, span.from); + if (span.to != null) to = Pos(found, span.to); + } + } + if (this.type == "bookmark") return from; + return from && {from: from, to: to}; + }; + + TextMarker.prototype.changed = function() { + var pos = this.find(), cm = this.doc.cm; + if (!pos || !cm) return; + if (this.type != "bookmark") pos = pos.from; + var line = getLine(this.doc, pos.line); + clearCachedMeasurement(cm, line); + if (pos.line >= cm.display.showingFrom && pos.line < cm.display.showingTo) { + for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) { + if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight); + break; + } + runInOp(cm, function() { + cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true; + }); + } + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + function markText(doc, from, to, options, type) { + if (options && options.shared) return markTextShared(doc, from, to, options, type); + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type); + if (type == "range" && !posLess(from, to)) return marker; + if (options) copyObj(options, marker); + if (marker.replacedWith) { + marker.collapsed = true; + marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true; + } + if (marker.collapsed) sawCollapsedSpans = true; + + if (marker.addToHistory) + addToHistory(doc, {from: from, to: to, origin: "markText"}, + {head: doc.sel.head, anchor: doc.sel.anchor}, NaN); + + var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine) + updateMaxLine = true; + var span = {from: null, to: null, marker: marker}; + size += line.text.length; + if (curLine == from.line) {span.from = from.ch; size -= from.ch;} + if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;} + if (marker.collapsed) { + if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch); + if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch); + else updateLineHeight(line, 0); + } + addMarkedSpan(line, span); + ++curLine; + }); + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + if (collapsedAtStart != collapsedAtEnd) + throw new Error("Inserting collapsed marker overlapping an existing one"); + marker.size = size; + marker.atomic = true; + } + if (cm) { + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed) + regChange(cm, from.line, to.line + 1); + if (marker.atomic) reCheckSelection(cm); + } + return marker; + } + + // SHARED TEXTMARKERS + + function SharedTextMarker(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0, me = this; i < markers.length; ++i) { + markers[i].parent = this; + on(markers[i], "clear", function(){me.clear();}); + } + } + CodeMirror.SharedTextMarker = SharedTextMarker; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function() { + return this.primary.find(); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.replacedWith; + linkedDocs(doc, function(doc) { + if (widget) options.replacedWith = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + // TEXTMARKER SPANS + + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || marker.type == "bookmark" && span.from == startCh && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push({from: span.from, + to: endsAfter ? null : span.to, + marker: marker}); + } + } + return nw; + } + + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh, + to: span.to == null ? null : span.to - endCh, + marker: marker}); + } + } + return nw; + } + + function stretchSpansOverChange(doc, change) { + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to); + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + if (sameLine && first) { + // Make sure we didn't create any zero-length spans + for (var i = 0; i < first.length; ++i) + if (first[i].from != null && first[i].from == first[i].to && first[i].marker.type != "bookmark") + first.splice(i--, 1); + if (!first.length) first = null; + } + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker}); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue; + var newParts = [j, 1]; + if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from)) + newParts.push({from: p.from, to: m.from}); + if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to)) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + function collapsedSpanAt(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if ((sp.from == null || sp.from < ch) && + (sp.to == null || sp.to > ch) && + (!found || found.width < sp.marker.width)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); } + function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); } + + function visualLine(doc, line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = getLine(doc, merged.find().from.line); + return line; + } + + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.replacedWith) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find().to, endLine = getLine(doc, end.line); + return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // LINE WIDGETS + + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.cm = cm; + this.node = node; + }; + eventMixin(LineWidget); + function widgetOperation(f) { + return function() { + var withOp = !this.cm.curOp; + if (withOp) startOperation(this.cm); + try {var result = f.apply(this, arguments);} + finally {if (withOp) endOperation(this.cm);} + return result; + }; + } + LineWidget.prototype.clear = widgetOperation(function() { + var ws = this.line.widgets, no = lineNo(this.line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) this.line.widgets = null; + var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop; + updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this))); + if (aboveVisible) addToScrollPos(this.cm, 0, -this.height); + regChange(this.cm, no, no + 1); + }); + LineWidget.prototype.changed = widgetOperation(function() { + var oldH = this.height; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(this.line, this.line.height + diff); + var no = lineNo(this.line); + regChange(this.cm, no, no + 1); + }); + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1) + removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative")); + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(cm, handle, node, options) { + var widget = new LineWidget(cm, node, options); + if (widget.noHScroll) cm.display.alignWidgets = true; + changeLine(cm, handle, function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) { + var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, 0, widget.height); + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + function runMode(cm, text, mode, state, f) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + if (text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + stream.pos = text.length; + style = null; + } else { + style = mode.token(stream, state); + } + if (!flattenSpans || curStyle != style) { + if (curStart < stream.start) f(stream.start, curStyle); + curStart = stream.start; curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + function highlightLine(cm, line, state) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen]; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) {st.push(end, style);}); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = cur ? cur + " " + style : style; + } + } + }); + } + + return st; + } + + function getLineStyles(cm, line) { + if (!line.styles || line.styles[0] != cm.state.modeGen) + line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. + function processLine(cm, line, state) { + var mode = cm.doc.mode; + var stream = new StringStream(line.text, cm.options.tabSize); + if (line.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + mode.token(stream, state); + stream.start = stream.pos; + } + } + + var styleToClassCache = {}; + function interpretTokenStyle(style, builder) { + if (!style) return null; + for (;;) { + var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/); + if (!lineClass) break; + style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (builder[prop] == null) + builder[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop])) + builder[prop] += " " + lineClass[2]; + } + return styleToClassCache[style] || + (styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-")); + } + + function buildLineContent(cm, realLine, measure, copyWidgets) { + var merged, line = realLine, empty = true; + while (merged = collapsedSpanAtStart(line)) + line = getLine(cm.doc, merged.find().from.line); + + var builder = {pre: elt("pre"), col: 0, pos: 0, + measure: null, measuredSomething: false, cm: cm, + copyWidgets: copyWidgets}; + + do { + if (line.text) empty = false; + builder.measure = line == realLine && measure; + builder.pos = 0; + builder.addToken = builder.measure ? buildTokenMeasure : buildToken; + if ((ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); + var next = insertLineContent(line, builder, getLineStyles(cm, line)); + if (measure && line == realLine && !builder.measuredSomething) { + measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); + builder.measuredSomething = true; + } + if (next) line = getLine(cm.doc, next.to.line); + } while (next); + + if (measure && !builder.measuredSomething && !measure[0]) + measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); + if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine)) + builder.pre.appendChild(document.createTextNode("\u00a0")); + + var order; + // Work around problem with the reported dimensions of single-char + // direction spans on IE (issue #1129). See also the comment in + // cursorCoords. + if (measure && ie && (order = getOrder(line))) { + var l = order.length - 1; + if (order[l].from == order[l].to) --l; + var last = order[l], prev = order[l - 1]; + if (last.from + 1 == last.to && prev && last.level < prev.level) { + var span = measure[builder.pos - 1]; + if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure), + span.nextSibling); + } + } + + var textClass = builder.textClass ? builder.textClass + " " + (realLine.textClass || "") : realLine.textClass; + if (textClass) builder.pre.className = textClass; + + signal(cm, "renderLine", cm, realLine, builder.pre); + return builder; + } + + var tokenSpecialChars = /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g; + function buildToken(builder, text, style, startStyle, endStyle, title) { + if (!text) return; + if (!tokenSpecialChars.test(text)) { + builder.col += text.length; + var content = document.createTextNode(text); + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + tokenSpecialChars.lastIndex = pos; + var m = tokenSpecialChars.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); + builder.col += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + builder.col += tabWidth; + } else { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + m[0].charCodeAt(0).toString(16); + content.appendChild(token); + builder.col += 1; + } + } + } + if (style || startStyle || endStyle || builder.measure) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle); + if (title) token.title = title; + return builder.pre.appendChild(token); + } + builder.pre.appendChild(content); + } + + function buildTokenMeasure(builder, text, style, startStyle, endStyle) { + var wrapping = builder.cm.options.lineWrapping; + for (var i = 0; i < text.length; ++i) { + var ch = text.charAt(i), start = i == 0; + if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) { + ch = text.slice(i, i + 2); + ++i; + } else if (i && wrapping && spanAffectsWrapping(text, i)) { + builder.pre.appendChild(elt("wbr")); + } + var old = builder.measure[builder.pos]; + var span = builder.measure[builder.pos] = + buildToken(builder, ch, style, + start && startStyle, i == text.length - 1 && endStyle); + if (old) span.leftSide = old.leftSide || old; + // In IE single-space nodes wrap differently than spaces + // embedded in larger text nodes, except when set to + // white-space: normal (issue #1268). + if (ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) && + i < text.length - 1 && !/\s/.test(text.charAt(i + 1))) + span.style.whiteSpace = "normal"; + builder.pos += ch.length; + } + if (text.length) builder.measuredSomething = true; + } + + function buildTokenSplitSpaces(inner) { + function split(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + return function(builder, text, style, startStyle, endStyle, title) { + return inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.replacedWith; + if (widget) { + if (builder.copyWidgets) widget = widget.cloneNode(true); + builder.pre.appendChild(widget); + if (builder.measure) { + if (size) { + builder.measure[builder.pos] = widget; + } else { + var elt = zeroWidthElement(builder.cm.display.measure); + if (marker.type == "bookmark" && !marker.insertLeft) + builder.measure[builder.pos] = builder.pre.appendChild(elt); + else if (builder.measure[builder.pos]) + return; + else + builder.measure[builder.pos] = builder.pre.insertBefore(elt, widget); + } + builder.measuredSomething = true; + } + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || collapsed.marker.size < m.size)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return collapsed.marker.find(); + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder); + } + } + } + + // DOCUMENT DATA STRUCTURE + + function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // First adjust the line structure + if (from.ch == 0 && to.ch == 0 && lastText == "") { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + for (var i = 0, e = text.length - 1, added = []; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + for (var added = [], i = 1, e = text.length - 1; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + for (var i = 1, e = text.length - 1, added = []; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + setSelection(doc, selAfter.anchor, selAfter.head, null, true); + } + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.history = makeHistory(); + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null}; + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start}); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue"}, + {head: top, anchor: top}, true); + }, + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + setLine: function(line, text) { + if (isLine(this, line)) + replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line))); + }, + removeLine: function(line) { + if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line))); + else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0))); + }, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(this, line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var sel = this.sel, pos; + if (start == null || start == "head") pos = sel.head; + else if (start == "anchor") pos = sel.anchor; + else if (start == "end" || start === false) pos = sel.to; + else pos = sel.from; + return copyPos(pos); + }, + somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);}, + + setCursor: docOperation(function(line, ch, extend) { + var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line); + if (extend) extendSelection(this, pos); + else setSelection(this, pos, pos); + }), + setSelection: docOperation(function(anchor, head, bias) { + setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), bias); + }), + extendSelection: docOperation(function(from, to, bias) { + extendSelection(this, clipPos(this, from), to && clipPos(this, to), bias); + }), + + getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);}, + replaceSelection: function(code, collapse, origin) { + makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around"); + }, + undo: docOperation(function() {makeChangeFromHistory(this, "undo");}), + redo: docOperation(function() {makeChangeFromHistory(this, "redo");}), + + setExtending: function(val) {this.sel.extend = val;}, + + historySize: function() { + var hist = this.history; + return {undo: hist.done.length, redo: hist.undone.length}; + }, + clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(); + }, + changeGeneration: function() { + this.history.lastOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = makeHistory(this.history.maxGeneration); + hist.done = histData.done.slice(0); + hist.undone = histData.undone.slice(0); + }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor, + shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn}; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = makeHistory(); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + Doc.prototype.eachLine = Doc.prototype.iter; + + // The Doc methods that should be available on CodeMirror instances + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) computeMaxLength(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + function getLine(chunk, n) { + n -= chunk.first; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + function updateLineHeight(line, height) { + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + function heightAtLine(cm, lineObj) { + lineObj = visualLine(cm.doc, lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function makeHistory(startGen) { + return { + // Arrays of history events. Doing something adds an event to + // done and clears undo. Undoing moves events from done to + // undone, redoing moves them in the other direction. + done: [], undone: [], undoDepth: Infinity, + // Used to track when changes can be merged into a single undo + // event + lastTime: 0, lastOp: null, lastOrigin: null, + // Used by the isClean() method + generation: startGen || 1, maxGeneration: startGen || 1 + }; + } + + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + function historyChangeFromChange(doc, change) { + var from = { line: change.from.line, ch: change.from.ch }; + var histChange = {from: from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + function addToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur = lst(hist.done); + + if (cur && + (hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*"))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (posEq(change.from, change.to) && posEq(change.from, last.to)) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head; + } else { + // Can not be merged, start a new event. + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation, + anchorBefore: doc.sel.anchor, headBefore: doc.sel.head, + anchorAfter: selAfter.anchor, headAfter: selAfter.head}; + hist.done.push(cur); + hist.generation = ++hist.maxGeneration; + while (hist.done.length > hist.undoDepth) + hist.done.shift(); + } + hist.lastTime = time; + hist.lastOp = opId; + hist.lastOrigin = change.origin; + } + + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i], changes = event.changes, newChanges = []; + copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore, + anchorAfter: event.anchorAfter, headAfter: event.headAfter}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSel(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); } + if (to < cur.from.line) { + cur.from.line += diff; + cur.to.line += diff; + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!sub.copied) { + sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore); + sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter); + sub.copied = true; + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } else { + rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore); + rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter); + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT OPERATORS + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + function on(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + } + + function signal(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + } + + var delayedCallbacks, delayedCallbackDepth = 0; + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + if (!delayedCallbacks) { + ++delayedCallbackDepth; + delayedCallbacks = []; + setTimeout(fireDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + delayedCallbacks.push(bnd(arr[i])); + } + + function signalDOMEvent(cm, e, override) { + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function fireDelayed() { + --delayedCallbackDepth; + var delayed = delayedCallbacks; + delayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal; + + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerCutOff = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + CodeMirror.countColumn = countColumn; + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + function selectInput(node) { + if (ios) { // Mobile Safari apparently has a bug where select() is broken. + node.selectionStart = 0; + node.selectionEnd = node.value.length; + } else { + // Suppress mysterious IE10 errors + try { node.select(); } + catch(_e) {} + } + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + + function createObj(base, props) { + function Obj() {} + Obj.prototype = base; + var inst = new Obj(); + if (props) copyObj(props, inst); + return inst; + } + + function copyObj(obj, target) { + if (!target) target = {}; + for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; + return target; + } + + function emptyArray(size) { + for (var a = [], i = 0; i < size; ++i) a.push(undefined); + return a; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordChar(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/; + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") setTextContent(e, content); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + function setTextContent(e, str) { + if (ie_lt9) { + e.innerHTML = ""; + e.appendChild(document.createTextNode(str)); + } else e.textContent = str; + } + + function getRect(node) { + return node.getBoundingClientRect(); + } + CodeMirror.replaceGetRect = function(f) { getRect = f; }; + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie_lt9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + // For a reason I have yet to figure out, some browsers disallow + // word wrapping between certain characters *only* if a new inline + // element is started between them. This makes it hard to reliably + // measure the position of things, since that requires inserting an + // extra span. This terribly fragile set of tests matches the + // character combinations that suffer from this phenomenon on the + // various browsers. + function spanAffectsWrapping() { return false; } + if (gecko) // Only for "$'" + spanAffectsWrapping = function(str, i) { + return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39; + }; + else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1)); + }; + else if (webkit && /Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + var code = str.charCodeAt(i - 1); + return code >= 8208 && code <= 8212; + }; + else if (webkit) + spanAffectsWrapping = function(str, i) { + if (i > 1 && str.charCodeAt(i - 1) == 45) { + if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true; + if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false; + } + return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|…[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1)); + }; + + var knownScrollbarWidth; + function scrollbarWidth(measure) { + if (knownScrollbarWidth != null) return knownScrollbarWidth; + var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll"); + removeChildrenAndAdd(measure, test); + if (test.offsetWidth) + knownScrollbarWidth = test.offsetHeight - test.clientHeight; + return knownScrollbarWidth || 0; + } + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8; + } + if (zwspSupported) return elt("span", "\u200b"); + else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == 'function'; + })(); + + // KEY NAMING + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete", + 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home", + 63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(cm.doc, line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line; + while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN))) + lineN = merged.find().to.line; + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN, ch); + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) { bidiOther = null; return i; } + if (cur.from == pos || cur.to == pos) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + bidiOther = found; + return i; + } else { + bidiOther = i; + return found; + } + } + } + bidiOther = null; + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar.test(line.text.charAt(pos))); + return pos; + } + + // This is somewhat involved. It is needed in order to move + // 'visually' through bi-directional text -- i.e., pressing left + // should make the cursor go left, even when in RTL text. The + // tricky part is the 'jumps', where RTL and LTR text touch each + // other. This often requires the cursor offset to move more than + // one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr"; + function charType(code) { + if (code <= 0xff) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600); + else if (0x700 <= code && code <= 0x8ac) return "r"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len - 1 ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push({from: start, to: i, level: 0}); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1}); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, {from: nstart, to: j, level: 2}); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1}); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift({from: 0, to: m[0].length, level: 0}); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push({from: len - m[0].length, to: len, level: 0}); + } + if (order[0].level != lst(order).level) + order.push({from: len, to: len, level: order[0].level}); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "3.19.0"; + + return CodeMirror; +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/css.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/css.js new file mode 100644 index 000000000..f47aba75a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/css.js @@ -0,0 +1,639 @@ +CodeMirror.defineMode("css", function(config, parserConfig) { + "use strict"; + + if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); + + var indentUnit = config.indentUnit || config.tabSize || 2, + hooks = parserConfig.hooks || {}, + atMediaTypes = parserConfig.atMediaTypes || {}, + atMediaFeatures = parserConfig.atMediaFeatures || {}, + propertyKeywords = parserConfig.propertyKeywords || {}, + colorKeywords = parserConfig.colorKeywords || {}, + valueKeywords = parserConfig.valueKeywords || {}, + allowNested = !!parserConfig.allowNested, + type = null; + + function ret(style, tp) { type = tp; return style; } + + function tokenBase(stream, state) { + var ch = stream.next(); + if (hooks[ch]) { + // result[0] is style and result[1] is type + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());} + else if (ch == "=") ret(null, "compare"); + else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare"); + else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } + else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } + else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } + else if (ch === "-") { + if (/\d/.test(stream.peek())) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (stream.match(/^[^-]+-/)) { + return ret("meta", "meta"); + } + } + else if (/[,+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } + else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { + return ret("qualifier", "qualifier"); + } + else if (ch == ":") { + return ret("operator", ch); + } + else if (/[;{}\[\]\(\)]/.test(ch)) { + return ret(null, ch); + } + else if (ch == "u" && stream.match("rl(")) { + stream.backUp(1); + state.tokenize = tokenParenthesized; + return ret("property", "variable"); + } + else { + stream.eatWhile(/[\w\\\-]/); + return ret("property", "variable"); + } + } + + function tokenString(quote, nonInclusive) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) + break; + escaped = !escaped && ch == "\\"; + } + if (!escaped) { + if (nonInclusive) stream.backUp(1); + state.tokenize = tokenBase; + } + return ret("string", "string"); + }; + } + + function tokenParenthesized(stream, state) { + stream.next(); // Must be '(' + if (!stream.match(/\s*[\"\']/, false)) + state.tokenize = tokenString(")", true); + else + state.tokenize = tokenBase; + return ret(null, "("); + } + + return { + startState: function(base) { + return {tokenize: tokenBase, + baseIndent: base || 0, + stack: [], + lastToken: null}; + }, + + token: function(stream, state) { + + // Use these terms when applicable (see http://www.xanthir.com/blog/b4E50) + // + // rule** or **ruleset: + // A selector + braces combo, or an at-rule. + // + // declaration block: + // A sequence of declarations. + // + // declaration: + // A property + colon + value combo. + // + // property value: + // The entire value of a property. + // + // component value: + // A single piece of a property value. Like the 5px in + // text-shadow: 0 0 5px blue;. Can also refer to things that are + // multiple terms, like the 1-4 terms that make up the background-size + // portion of the background shorthand. + // + // term: + // The basic unit of author-facing CSS, like a single number (5), + // dimension (5px), string ("foo"), or function. Officially defined + // by the CSS 2.1 grammar (look for the 'term' production) + // + // + // simple selector: + // A single atomic selector, like a type selector, an attr selector, a + // class selector, etc. + // + // compound selector: + // One or more simple selectors without a combinator. div.example is + // compound, div > .example is not. + // + // complex selector: + // One or more compound selectors chained with combinators. + // + // combinator: + // The parts of selectors that express relationships. There are four + // currently - the space (descendant combinator), the greater-than + // bracket (child combinator), the plus sign (next sibling combinator), + // and the tilda (following sibling combinator). + // + // sequence of selectors: + // One or more of the named type of selector chained with commas. + + state.tokenize = state.tokenize || tokenBase; + if (state.tokenize == tokenBase && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (style && typeof style != "string") style = ret(style[0], style[1]); + + // Changing style returned based on context + var context = state.stack[state.stack.length-1]; + if (style == "variable") { + if (type == "variable-definition") state.stack.push("propertyValue"); + return state.lastToken = "variable-2"; + } else if (style == "property") { + var word = stream.current().toLowerCase(); + if (context == "propertyValue") { + if (valueKeywords.hasOwnProperty(word)) { + style = "string-2"; + } else if (colorKeywords.hasOwnProperty(word)) { + style = "keyword"; + } else { + style = "variable-2"; + } + } else if (context == "rule") { + if (!propertyKeywords.hasOwnProperty(word)) { + style += " error"; + } + } else if (context == "block") { + // if a value is present in both property, value, or color, the order + // of preference is property -> color -> value + if (propertyKeywords.hasOwnProperty(word)) { + style = "property"; + } else if (colorKeywords.hasOwnProperty(word)) { + style = "keyword"; + } else if (valueKeywords.hasOwnProperty(word)) { + style = "string-2"; + } else { + style = "tag"; + } + } else if (!context || context == "@media{") { + style = "tag"; + } else if (context == "@media") { + if (atMediaTypes[stream.current()]) { + style = "attribute"; // Known attribute + } else if (/^(only|not)$/.test(word)) { + style = "keyword"; + } else if (word == "and") { + style = "error"; // "and" is only allowed in @mediaType + } else if (atMediaFeatures.hasOwnProperty(word)) { + style = "error"; // Known property, should be in @mediaType( + } else { + // Unknown, expecting keyword or attribute, assuming attribute + style = "attribute error"; + } + } else if (context == "@mediaType") { + if (atMediaTypes.hasOwnProperty(word)) { + style = "attribute"; + } else if (word == "and") { + style = "operator"; + } else if (/^(only|not)$/.test(word)) { + style = "error"; // Only allowed in @media + } else { + // Unknown attribute or property, but expecting property (preceded + // by "and"). Should be in parentheses + style = "error"; + } + } else if (context == "@mediaType(") { + if (propertyKeywords.hasOwnProperty(word)) { + // do nothing, remains "property" + } else if (atMediaTypes.hasOwnProperty(word)) { + style = "error"; // Known property, should be in parentheses + } else if (word == "and") { + style = "operator"; + } else if (/^(only|not)$/.test(word)) { + style = "error"; // Only allowed in @media + } else { + style += " error"; + } + } else if (context == "@import") { + style = "tag"; + } else { + style = "error"; + } + } else if (style == "atom") { + if(!context || context == "@media{" || context == "block") { + style = "builtin"; + } else if (context == "propertyValue") { + if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) { + style += " error"; + } + } else { + style = "error"; + } + } else if (context == "@media" && type == "{") { + style = "error"; + } + + // Push/pop context stack + if (type == "{") { + if (context == "@media" || context == "@mediaType") { + state.stack[state.stack.length-1] = "@media{"; + } + else { + var newContext = allowNested ? "block" : "rule"; + state.stack.push(newContext); + } + } + else if (type == "}") { + if (context == "interpolation") style = "operator"; + // Pop off end of array until { is reached + while(state.stack.length){ + var removed = state.stack.pop(); + if(removed.indexOf("{") > -1){ + break; + } + } + } + else if (type == "interpolation") state.stack.push("interpolation"); + else if (type == "@media") state.stack.push("@media"); + else if (type == "@import") state.stack.push("@import"); + else if (context == "@media" && /\b(keyword|attribute)\b/.test(style)) + state.stack[state.stack.length-1] = "@mediaType"; + else if (context == "@mediaType" && stream.current() == ",") + state.stack[state.stack.length-1] = "@media"; + else if (type == "(") { + if (context == "@media" || context == "@mediaType") { + // Make sure @mediaType is used to avoid error on { + state.stack[state.stack.length-1] = "@mediaType"; + state.stack.push("@mediaType("); + } + else state.stack.push("("); + } + else if (type == ")") { + // Pop off end of array until ( is reached + while(state.stack.length){ + var removed = state.stack.pop(); + if(removed.indexOf("(") > -1){ + break; + } + } + } + else if (type == ":" && state.lastToken == "property") state.stack.push("propertyValue"); + else if (context == "propertyValue" && type == ";") state.stack.pop(); + else if (context == "@import" && type == ";") state.stack.pop(); + + return state.lastToken = style; + }, + + indent: function(state, textAfter) { + var n = state.stack.length; + if (/^\}/.test(textAfter)) + n -= state.stack[n-1] == "propertyValue" ? 2 : 1; + return state.baseIndent + n * indentUnit; + }, + + electricChars: "}", + blockCommentStart: "/*", + blockCommentEnd: "*/", + fold: "brace" + }; +}); + +(function() { + function keySet(array) { + var keys = {}; + for (var i = 0; i < array.length; ++i) { + keys[array[i]] = true; + } + return keys; + } + + var atMediaTypes = keySet([ + "all", "aural", "braille", "handheld", "print", "projection", "screen", + "tty", "tv", "embossed" + ]); + + var atMediaFeatures = keySet([ + "width", "min-width", "max-width", "height", "min-height", "max-height", + "device-width", "min-device-width", "max-device-width", "device-height", + "min-device-height", "max-device-height", "aspect-ratio", + "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", + "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", + "max-color", "color-index", "min-color-index", "max-color-index", + "monochrome", "min-monochrome", "max-monochrome", "resolution", + "min-resolution", "max-resolution", "scan", "grid" + ]); + + var propertyKeywords = keySet([ + "align-content", "align-items", "align-self", "alignment-adjust", + "alignment-baseline", "anchor-point", "animation", "animation-delay", + "animation-direction", "animation-duration", "animation-iteration-count", + "animation-name", "animation-play-state", "animation-timing-function", + "appearance", "azimuth", "backface-visibility", "background", + "background-attachment", "background-clip", "background-color", + "background-image", "background-origin", "background-position", + "background-repeat", "background-size", "baseline-shift", "binding", + "bleed", "bookmark-label", "bookmark-level", "bookmark-state", + "bookmark-target", "border", "border-bottom", "border-bottom-color", + "border-bottom-left-radius", "border-bottom-right-radius", + "border-bottom-style", "border-bottom-width", "border-collapse", + "border-color", "border-image", "border-image-outset", + "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-left", "border-left-color", + "border-left-style", "border-left-width", "border-radius", "border-right", + "border-right-color", "border-right-style", "border-right-width", + "border-spacing", "border-style", "border-top", "border-top-color", + "border-top-left-radius", "border-top-right-radius", "border-top-style", + "border-top-width", "border-width", "bottom", "box-decoration-break", + "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", + "caption-side", "clear", "clip", "color", "color-profile", "column-count", + "column-fill", "column-gap", "column-rule", "column-rule-color", + "column-rule-style", "column-rule-width", "column-span", "column-width", + "columns", "content", "counter-increment", "counter-reset", "crop", "cue", + "cue-after", "cue-before", "cursor", "direction", "display", + "dominant-baseline", "drop-initial-after-adjust", + "drop-initial-after-align", "drop-initial-before-adjust", + "drop-initial-before-align", "drop-initial-size", "drop-initial-value", + "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis", + "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", + "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings", + "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust", + "font-stretch", "font-style", "font-synthesis", "font-variant", + "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", + "font-variant-ligatures", "font-variant-numeric", "font-variant-position", + "font-weight", "grid-cell", "grid-column", "grid-column-align", + "grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow", + "grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span", + "grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens", + "icon", "image-orientation", "image-rendering", "image-resolution", + "inline-box-align", "justify-content", "left", "letter-spacing", + "line-break", "line-height", "line-stacking", "line-stacking-ruby", + "line-stacking-shift", "line-stacking-strategy", "list-style", + "list-style-image", "list-style-position", "list-style-type", "margin", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "marker-offset", "marks", "marquee-direction", "marquee-loop", + "marquee-play-count", "marquee-speed", "marquee-style", "max-height", + "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", + "nav-left", "nav-right", "nav-up", "opacity", "order", "orphans", "outline", + "outline-color", "outline-offset", "outline-style", "outline-width", + "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y", + "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", + "page", "page-break-after", "page-break-before", "page-break-inside", + "page-policy", "pause", "pause-after", "pause-before", "perspective", + "perspective-origin", "pitch", "pitch-range", "play-during", "position", + "presentation-level", "punctuation-trim", "quotes", "region-break-after", + "region-break-before", "region-break-inside", "region-fragment", + "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness", + "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", + "ruby-position", "ruby-span", "shape-inside", "shape-outside", "size", + "speak", "speak-as", "speak-header", + "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", + "tab-size", "table-layout", "target", "target-name", "target-new", + "target-position", "text-align", "text-align-last", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-skip", + "text-decoration-style", "text-emphasis", "text-emphasis-color", + "text-emphasis-position", "text-emphasis-style", "text-height", + "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow", + "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position", + "text-wrap", "top", "transform", "transform-origin", "transform-style", + "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "unicode-bidi", + "vertical-align", "visibility", "voice-balance", "voice-duration", + "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", + "voice-volume", "volume", "white-space", "widows", "width", "word-break", + "word-spacing", "word-wrap", "z-index", "zoom", + // SVG-specific + "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", + "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", + "color-interpolation", "color-interpolation-filters", "color-profile", + "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", + "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", + "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", + "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", + "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", + "glyph-orientation-vertical", "kerning", "text-anchor", "writing-mode" + ]); + + var colorKeywords = keySet([ + "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", + "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", + "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", + "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", + "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", + "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", + "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", + "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", + "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", + "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", + "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", + "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", + "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", + "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", + "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", + "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", + "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", + "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", + "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", + "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", + "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", + "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", + "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", + "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", + "whitesmoke", "yellow", "yellowgreen" + ]); + + var valueKeywords = keySet([ + "above", "absolute", "activeborder", "activecaption", "afar", + "after-white-space", "ahead", "alias", "all", "all-scroll", "alternate", + "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", + "arabic-indic", "armenian", "asterisks", "auto", "avoid", "avoid-column", "avoid-page", + "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", + "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", + "both", "bottom", "break", "break-all", "break-word", "button", "button-bevel", + "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian", + "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", + "cell", "center", "checkbox", "circle", "cjk-earthly-branch", + "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", + "col-resize", "collapse", "column", "compact", "condensed", "contain", "content", + "content-box", "context-menu", "continuous", "copy", "cover", "crop", + "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal", + "decimal-leading-zero", "default", "default-button", "destination-atop", + "destination-in", "destination-out", "destination-over", "devanagari", + "disc", "discard", "document", "dot-dash", "dot-dot-dash", "dotted", + "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", + "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", + "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", + "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", + "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", + "ethiopic-halehame-gez", "ethiopic-halehame-om-et", + "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", + "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", + "ethiopic-halehame-tig", "ew-resize", "expanded", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes", + "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", + "help", "hidden", "hide", "higher", "highlight", "highlighttext", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", + "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", + "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", + "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert", + "italic", "justify", "kannada", "katakana", "katakana-iroha", "keep-all", "khmer", + "landscape", "lao", "large", "larger", "left", "level", "lighter", + "line-through", "linear", "lines", "list-item", "listbox", "listitem", + "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", + "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", + "lower-roman", "lowercase", "ltr", "malayalam", "match", + "media-controls-background", "media-current-time-display", + "media-fullscreen-button", "media-mute-button", "media-play-button", + "media-return-to-realtime-button", "media-rewind-button", + "media-seek-back-button", "media-seek-forward-button", "media-slider", + "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", + "media-volume-slider-container", "media-volume-sliderthumb", "medium", + "menu", "menulist", "menulist-button", "menulist-text", + "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", + "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize", + "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", + "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", + "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", + "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", + "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", + "painted", "page", "paused", "persian", "plus-darker", "plus-lighter", "pointer", + "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button", + "radio", "read-only", "read-write", "read-write-plaintext-only", "rectangle", "region", + "relative", "repeat", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba", + "ridge", "right", "round", "row-resize", "rtl", "run-in", "running", + "s-resize", "sans-serif", "scroll", "scrollbar", "se-resize", "searchfield", + "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", + "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", + "single", "skip-white-space", "slide", "slider-horizontal", + "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", + "small", "small-caps", "small-caption", "smaller", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "square", + "square-button", "start", "static", "status-bar", "stretch", "stroke", + "sub", "subpixel-antialiased", "super", "sw-resize", "table", + "table-caption", "table-cell", "table-column", "table-column-group", + "table-footer-group", "table-header-group", "table-row", "table-row-group", + "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", + "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", + "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", + "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", + "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", + "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", + "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", + "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", + "window", "windowframe", "windowtext", "x-large", "x-small", "xor", + "xx-large", "xx-small" + ]); + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return ["comment", "comment"]; + } + + CodeMirror.defineMIME("text/css", { + atMediaTypes: atMediaTypes, + atMediaFeatures: atMediaFeatures, + propertyKeywords: propertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + hooks: { + "<": function(stream, state) { + function tokenSGMLComment(stream, state) { + var dashes = 0, ch; + while ((ch = stream.next()) != null) { + if (dashes >= 2 && ch == ">") { + state.tokenize = null; + break; + } + dashes = (ch == "-") ? dashes + 1 : 0; + } + return ["comment", "comment"]; + } + if (stream.eat("!")) { + state.tokenize = tokenSGMLComment; + return tokenSGMLComment(stream, state); + } + }, + "/": function(stream, state) { + if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + return false; + } + }, + name: "css" + }); + + CodeMirror.defineMIME("text/x-scss", { + atMediaTypes: atMediaTypes, + atMediaFeatures: atMediaFeatures, + propertyKeywords: propertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + allowNested: true, + hooks: { + ":": function(stream) { + if (stream.match(/\s*{/)) { + return [null, "{"]; + } + return false; + }, + "$": function(stream) { + stream.match(/^[\w-]+/); + if (stream.peek() == ":") { + return ["variable", "variable-definition"]; + } + return ["variable", "variable"]; + }, + ",": function(_stream, state) { + if (state.stack[state.stack.length - 1] == "propertyValue") { + return ["operator", ";"]; + } + }, + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + "#": function(stream) { + if (stream.eat("{")) { + return ["operator", "interpolation"]; + } else { + stream.eatWhile(/[\w\\\-]/); + return ["atom", "hash"]; + } + } + }, + name: "css" + }); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/index.html new file mode 100644 index 000000000..1d1865e2e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/index.html @@ -0,0 +1,70 @@ + + +CodeMirror: CSS mode + + + + + + + + + +
+

CSS mode

+
+ + +

MIME types defined: text/css.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss.html new file mode 100644 index 000000000..72781c0dc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss.html @@ -0,0 +1,157 @@ + + +CodeMirror: SCSS mode + + + + + + + + + +
+

SCSS mode

+
+ + +

MIME types defined: text/scss.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss_test.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss_test.js new file mode 100644 index 000000000..bc2a07857 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/scss_test.js @@ -0,0 +1,84 @@ +(function() { + var mode = CodeMirror.getMode({tabSize: 1}, "text/x-scss"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } + function IT(name) { test.indentation(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } + + MT('url_with_quotation', + "[tag foo] { [property background][operator :][string-2 url]([string test.jpg]) }"); + + MT('url_with_double_quotes', + "[tag foo] { [property background][operator :][string-2 url]([string \"test.jpg\"]) }"); + + MT('url_with_single_quotes', + "[tag foo] { [property background][operator :][string-2 url]([string \'test.jpg\']) }"); + + MT('string', + "[def @import] [string \"compass/css3\"]"); + + MT('important_keyword', + "[tag foo] { [property background][operator :][string-2 url]([string \'test.jpg\']) [keyword !important] }"); + + MT('variable', + "[variable-2 $blue][operator :][atom #333]"); + + MT('variable_as_attribute', + "[tag foo] { [property color][operator :][variable-2 $blue] }"); + + MT('numbers', + "[tag foo] { [property padding][operator :][number 10px] [number 10] [number 10em] [number 8in] }"); + + MT('number_percentage', + "[tag foo] { [property width][operator :][number 80%] }"); + + MT('selector', + "[builtin #hello][qualifier .world]{}"); + + MT('singleline_comment', + "[comment // this is a comment]"); + + MT('multiline_comment', + "[comment /*foobar*/]"); + + MT('attribute_with_hyphen', + "[tag foo] { [property font-size][operator :][number 10px] }"); + + MT('string_after_attribute', + "[tag foo] { [property content][operator :][string \"::\"] }"); + + MT('directives', + "[def @include] [qualifier .mixin]"); + + MT('basic_structure', + "[tag p] { [property background][operator :][keyword red]; }"); + + MT('nested_structure', + "[tag p] { [tag a] { [property color][operator :][keyword red]; } }"); + + MT('mixin', + "[def @mixin] [tag table-base] {}"); + + MT('number_without_semicolon', + "[tag p] {[property width][operator :][number 12]}", + "[tag a] {[property color][operator :][keyword red];}"); + + MT('atom_in_nested_block', + "[tag p] { [tag a] { [property color][operator :][atom #000]; } }"); + + MT('interpolation_in_property', + "[tag foo] { [operator #{][variable-2 $hello][operator }:][number 2]; }"); + + MT('interpolation_in_selector', + "[tag foo][operator #{][variable-2 $hello][operator }] { [property color][operator :][atom #000]; }"); + + MT('interpolation_error', + "[tag foo][operator #{][error foo][operator }] { [property color][operator :][atom #000]; }"); + + MT("divide_operator", + "[tag foo] { [property width][operator :][number 4] [operator /] [number 2] }"); + + MT('nested_structure_with_id_selector', + "[tag p] { [builtin #hello] { [property color][operator :][keyword red]; } }"); + + IT('mixin', + "@mixin container[1 (][2 $a: 10][1 , ][2 $b: 10][1 , ][2 $c: 10]) [1 {]}"); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/test.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/test.js new file mode 100644 index 000000000..43647833a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/css/test.js @@ -0,0 +1,130 @@ +(function() { + var mode = CodeMirror.getMode({tabSize: 1}, "css"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + function IT(name) { test.indentation(name, mode, Array.prototype.slice.call(arguments, 1)); } + + // Requires at least one media query + MT("atMediaEmpty", + "[def @media] [error {] }"); + + MT("atMediaMultiple", + "[def @media] [keyword not] [attribute screen] [operator and] ([property color]), [keyword not] [attribute print] [operator and] ([property color]) { }"); + + MT("atMediaCheckStack", + "[def @media] [attribute screen] { } [tag foo] { }"); + + MT("atMediaCheckStack", + "[def @media] [attribute screen] ([property color]) { } [tag foo] { }"); + + MT("atMediaPropertyOnly", + "[def @media] ([property color]) { } [tag foo] { }"); + + MT("atMediaCheckStackInvalidAttribute", + "[def @media] [attribute&error foobarhello] { [tag foo] { } }"); + + MT("atMediaCheckStackInvalidAttribute", + "[def @media] [attribute&error foobarhello] { } [tag foo] { }"); + + // Error, because "and" is only allowed immediately preceding a media expression + MT("atMediaInvalidAttribute", + "[def @media] [attribute&error foobarhello] { }"); + + // Error, because "and" is only allowed immediately preceding a media expression + MT("atMediaInvalidAnd", + "[def @media] [error and] [attribute screen] { }"); + + // Error, because "not" is only allowed as the first item in each media query + MT("atMediaInvalidNot", + "[def @media] [attribute screen] [error not] ([error not]) { }"); + + // Error, because "only" is only allowed as the first item in each media query + MT("atMediaInvalidOnly", + "[def @media] [attribute screen] [error only] ([error only]) { }"); + + // Error, because "foobarhello" is neither a known type or property, but + // property was expected (after "and"), and it should be in parenthese. + MT("atMediaUnknownType", + "[def @media] [attribute screen] [operator and] [error foobarhello] { }"); + + // Error, because "color" is not a known type, but is a known property, and + // should be in parentheses. + MT("atMediaInvalidType", + "[def @media] [attribute screen] [operator and] [error color] { }"); + + // Error, because "print" is not a known property, but is a known type, + // and should not be in parenthese. + MT("atMediaInvalidProperty", + "[def @media] [attribute screen] [operator and] ([error print]) { }"); + + // Soft error, because "foobarhello" is not a known property or type. + MT("atMediaUnknownProperty", + "[def @media] [attribute screen] [operator and] ([property&error foobarhello]) { }"); + + // Make sure nesting works with media queries + MT("atMediaMaxWidthNested", + "[def @media] [attribute screen] [operator and] ([property max-width][operator :] [number 25px]) { [tag foo] { } }"); + + MT("tagSelector", + "[tag foo] { }"); + + MT("classSelector", + "[qualifier .foo-bar_hello] { }"); + + MT("idSelector", + "[builtin #foo] { [error #foo] }"); + + MT("tagSelectorUnclosed", + "[tag foo] { [property margin][operator :] [number 0] } [tag bar] { }"); + + MT("tagStringNoQuotes", + "[tag foo] { [property font-family][operator :] [variable-2 hello] [variable-2 world]; }"); + + MT("tagStringDouble", + "[tag foo] { [property font-family][operator :] [string \"hello world\"]; }"); + + MT("tagStringSingle", + "[tag foo] { [property font-family][operator :] [string 'hello world']; }"); + + MT("tagColorKeyword", + "[tag foo] {" + + "[property color][operator :] [keyword black];" + + "[property color][operator :] [keyword navy];" + + "[property color][operator :] [keyword yellow];" + + "}"); + + MT("tagColorHex3", + "[tag foo] { [property background][operator :] [atom #fff]; }"); + + MT("tagColorHex6", + "[tag foo] { [property background][operator :] [atom #ffffff]; }"); + + MT("tagColorHex4", + "[tag foo] { [property background][operator :] [atom&error #ffff]; }"); + + MT("tagColorHexInvalid", + "[tag foo] { [property background][operator :] [atom&error #ffg]; }"); + + MT("tagNegativeNumber", + "[tag foo] { [property margin][operator :] [number -5px]; }"); + + MT("tagPositiveNumber", + "[tag foo] { [property padding][operator :] [number 5px]; }"); + + MT("tagVendor", + "[tag foo] { [meta -foo-][property box-sizing][operator :] [meta -foo-][string-2 border-box]; }"); + + MT("tagBogusProperty", + "[tag foo] { [property&error barhelloworld][operator :] [number 0]; }"); + + MT("tagTwoProperties", + "[tag foo] { [property margin][operator :] [number 0]; [property padding][operator :] [number 0]; }"); + + MT("tagTwoPropertiesURL", + "[tag foo] { [property background][operator :] [string-2 url]([string //example.com/foo.png]); [property padding][operator :] [number 0]; }"); + + MT("commentSGML", + "[comment ]"); + + IT("tagSelector", + "strong, em [1 { background][2 : rgba][3 (255, 255, 0, .2][2 )][1 ;]}"); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/htmlembedded.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/htmlembedded.js new file mode 100644 index 000000000..ff6dfd2f9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/htmlembedded.js @@ -0,0 +1,73 @@ +CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { + + //config settings + var scriptStartRegex = parserConfig.scriptStartRegex || /^<%/i, + scriptEndRegex = parserConfig.scriptEndRegex || /^%>/i; + + //inner modes + var scriptingMode, htmlMixedMode; + + //tokenizer when in html mode + function htmlDispatch(stream, state) { + if (stream.match(scriptStartRegex, false)) { + state.token=scriptingDispatch; + return scriptingMode.token(stream, state.scriptState); + } + else + return htmlMixedMode.token(stream, state.htmlState); + } + + //tokenizer when in scripting mode + function scriptingDispatch(stream, state) { + if (stream.match(scriptEndRegex, false)) { + state.token=htmlDispatch; + return htmlMixedMode.token(stream, state.htmlState); + } + else + return scriptingMode.token(stream, state.scriptState); + } + + + return { + startState: function() { + scriptingMode = scriptingMode || CodeMirror.getMode(config, parserConfig.scriptingModeSpec); + htmlMixedMode = htmlMixedMode || CodeMirror.getMode(config, "htmlmixed"); + return { + token : parserConfig.startOpen ? scriptingDispatch : htmlDispatch, + htmlState : CodeMirror.startState(htmlMixedMode), + scriptState : CodeMirror.startState(scriptingMode) + }; + }, + + token: function(stream, state) { + return state.token(stream, state); + }, + + indent: function(state, textAfter) { + if (state.token == htmlDispatch) + return htmlMixedMode.indent(state.htmlState, textAfter); + else if (scriptingMode.indent) + return scriptingMode.indent(state.scriptState, textAfter); + }, + + copyState: function(state) { + return { + token : state.token, + htmlState : CodeMirror.copyState(htmlMixedMode, state.htmlState), + scriptState : CodeMirror.copyState(scriptingMode, state.scriptState) + }; + }, + + electricChars: "/{}:", + + innerMode: function(state) { + if (state.token == scriptingDispatch) return {state: state.scriptState, mode: scriptingMode}; + else return {state: state.htmlState, mode: htmlMixedMode}; + } + }; +}, "htmlmixed"); + +CodeMirror.defineMIME("application/x-ejs", { name: "htmlembedded", scriptingModeSpec:"javascript"}); +CodeMirror.defineMIME("application/x-aspx", { name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); +CodeMirror.defineMIME("application/x-jsp", { name: "htmlembedded", scriptingModeSpec:"text/x-java"}); +CodeMirror.defineMIME("application/x-erb", { name: "htmlembedded", scriptingModeSpec:"ruby"}); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/index.html new file mode 100644 index 000000000..4ab90f7d8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlembedded/index.html @@ -0,0 +1,60 @@ + + +CodeMirror: Html Embedded Scripts mode + + + + + + + + + + + + + +
+

Html Embedded Scripts mode

+
+ + + +

Mode for html embedded scripts like JSP and ASP.NET. Depends on HtmlMixed which in turn depends on + JavaScript, CSS and XML.
Other dependancies include those of the scriping language chosen.

+ +

MIME types defined: application/x-aspx (ASP.NET), + application/x-ejs (Embedded Javascript), application/x-jsp (JavaServer Pages)

+
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/htmlmixed.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/htmlmixed.js new file mode 100644 index 000000000..ec0c21d24 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/htmlmixed.js @@ -0,0 +1,104 @@ +CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); + var cssMode = CodeMirror.getMode(config, "css"); + + var scriptTypes = [], scriptTypesConf = parserConfig && parserConfig.scriptTypes; + scriptTypes.push({matches: /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, + mode: CodeMirror.getMode(config, "javascript")}); + if (scriptTypesConf) for (var i = 0; i < scriptTypesConf.length; ++i) { + var conf = scriptTypesConf[i]; + scriptTypes.push({matches: conf.matches, mode: conf.mode && CodeMirror.getMode(config, conf.mode)}); + } + scriptTypes.push({matches: /./, + mode: CodeMirror.getMode(config, "text/plain")}); + + function html(stream, state) { + var tagName = state.htmlState.tagName; + var style = htmlMode.token(stream, state.htmlState); + if (tagName == "script" && /\btag\b/.test(style) && stream.current() == ">") { + // Script block: mode to change to depends on type attribute + var scriptType = stream.string.slice(Math.max(0, stream.pos - 100), stream.pos).match(/\btype\s*=\s*("[^"]+"|'[^']+'|\S+)[^<]*$/i); + scriptType = scriptType ? scriptType[1] : ""; + if (scriptType && /[\"\']/.test(scriptType.charAt(0))) scriptType = scriptType.slice(1, scriptType.length - 1); + for (var i = 0; i < scriptTypes.length; ++i) { + var tp = scriptTypes[i]; + if (typeof tp.matches == "string" ? scriptType == tp.matches : tp.matches.test(scriptType)) { + if (tp.mode) { + state.token = script; + state.localMode = tp.mode; + state.localState = tp.mode.startState && tp.mode.startState(htmlMode.indent(state.htmlState, "")); + } + break; + } + } + } else if (tagName == "style" && /\btag\b/.test(style) && stream.current() == ">") { + state.token = css; + state.localMode = cssMode; + state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); + } + return style; + } + function maybeBackup(stream, pat, style) { + var cur = stream.current(); + var close = cur.search(pat), m; + if (close > -1) stream.backUp(cur.length - close); + else if (m = cur.match(/<\/?$/)) { + stream.backUp(cur.length); + if (!stream.match(pat, false)) stream.match(cur[0]); + } + return style; + } + function script(stream, state) { + if (stream.match(/^<\/\s*script\s*>/i, false)) { + state.token = html; + state.localState = state.localMode = null; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*script\s*>/, + state.localMode.token(stream, state.localState)); + } + function css(stream, state) { + if (stream.match(/^<\/\s*style\s*>/i, false)) { + state.token = html; + state.localState = state.localMode = null; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*style\s*>/, + cssMode.token(stream, state.localState)); + } + + return { + startState: function() { + var state = htmlMode.startState(); + return {token: html, localMode: null, localState: null, htmlState: state}; + }, + + copyState: function(state) { + if (state.localState) + var local = CodeMirror.copyState(state.localMode, state.localState); + return {token: state.token, localMode: state.localMode, localState: local, + htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; + }, + + token: function(stream, state) { + return state.token(stream, state); + }, + + indent: function(state, textAfter) { + if (!state.localMode || /^\s*<\//.test(textAfter)) + return htmlMode.indent(state.htmlState, textAfter); + else if (state.localMode.indent) + return state.localMode.indent(state.localState, textAfter); + else + return CodeMirror.Pass; + }, + + electricChars: "/{}:", + + innerMode: function(state) { + return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; + } + }; +}, "xml", "javascript", "css"); + +CodeMirror.defineMIME("text/html", "htmlmixed"); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/index.html new file mode 100644 index 000000000..b46174351 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/htmlmixed/index.html @@ -0,0 +1,85 @@ + + +CodeMirror: HTML mixed mode + + + + + + + + + + + + + +
+

HTML mixed mode

+
+ + +

The HTML mixed mode depends on the XML, JavaScript, and CSS modes.

+ +

It takes an optional mode configuration + option, scriptTypes, which can be used to add custom + behavior for specific <script type="..."> tags. If + given, it should hold an array of {matches, mode} + objects, where matches is a string or regexp that + matches the script type, and mode is + either null, for script types that should stay in + HTML mode, or a mode + spec corresponding to the mode that should be used for the + script.

+ +

MIME types defined: text/html + (redefined, only takes effect if you load this parser after the + XML parser).

+ +
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/index.html new file mode 100644 index 000000000..45d70ffdb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/index.html @@ -0,0 +1,107 @@ + + +CodeMirror: JavaScript mode + + + + + + + + + + + + +
+

JavaScript mode

+ + +
+ + + +

+ JavaScript mode supports a two configuration + options: +

    +
  • json which will set the mode to expect JSON + data rather than a JavaScript program.
  • +
  • typescript which will activate additional + syntax highlighting and some other things for TypeScript code + (demo).
  • +
  • statementIndent which (given a number) will + determine the amount of indentation to use for statements + continued on a new line.
  • +
+

+ +

MIME types defined: text/javascript, application/json, text/typescript, application/typescript.

+
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/javascript.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/javascript.js new file mode 100644 index 000000000..e8ace32d8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/javascript.js @@ -0,0 +1,480 @@ +// TODO actually recognize syntax of TypeScript constructs + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonMode = parserConfig.json; + var isTS = parserConfig.typescript; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + var jsKeywords = { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, + "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this") + }; + + // Extend the 'normal' keywords with the TypeScript language extensions + if (isTS) { + var type = {type: "variable", style: "variable-3"}; + var tsKeywords = { + // object-like things + "interface": kw("interface"), + "class": kw("class"), + "extends": kw("extends"), + "constructor": kw("constructor"), + + // scope modifiers + "public": kw("public"), + "private": kw("private"), + "protected": kw("protected"), + "static": kw("static"), + + "super": kw("super"), + + // types + "string": type, "number": type, "bool": type, "any": type + }; + + for (var attr in tsKeywords) { + jsKeywords[attr] = tsKeywords[attr]; + } + } + + return jsKeywords; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^]/; + + function chain(stream, state, f) { + state.tokenize = f; + return f(stream, state); + } + + function nextUntilUnescaped(stream, end) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (next == end && !escaped) + return false; + escaped = !escaped && next == "\\"; + } + return escaped; + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function jsTokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") + return chain(stream, state, jsTokenString(ch)); + else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) + return ret("number", "number"); + else if (/[\[\]{}\(\),;\:\.]/.test(ch)) + return ret(ch); + else if (ch == "0" && stream.eat(/x/i)) { + stream.eatWhile(/[\da-f]/i); + return ret("number", "number"); + } + else if (/\d/.test(ch)) { + stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + return ret("number", "number"); + } + else if (ch == "/") { + if (stream.eat("*")) { + return chain(stream, state, jsTokenComment); + } + else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } + else if (state.lastType == "operator" || state.lastType == "keyword c" || + /^[\[{}\(,;:]$/.test(state.lastType)) { + nextUntilUnescaped(stream, "/"); + stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla + return ret("regexp", "string-2"); + } + else { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + } + else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } + else if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + else { + stream.eatWhile(/[\w\$_]/); + var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; + return (known && state.lastType != ".") ? ret(known.type, known.style, word) : + ret("variable", "variable", word); + } + } + + function jsTokenString(quote) { + return function(stream, state) { + if (!nextUntilUnescaped(stream, quote)) + state.tokenize = jsTokenBase; + return ret("string", "string"); + }; + } + + function jsTokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = jsTokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function register(varname) { + function inList(list) { + for (var v = list; v; v = v.next) + if (v.name == varname) return true; + return false; + } + var state = cx.state; + if (state.context) { + cx.marked = "def"; + if (inList(state.localVars)) return; + state.localVars = {name: varname, next: state.localVars}; + } else { + if (inList(state.globalVars)) return; + state.globalVars = {name: varname, next: state.globalVars}; + } + } + + // Combinators + + var defaultVars = {name: "this", next: {name: "arguments"}}; + function pushcontext() { + cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + cx.state.localVars = defaultVars; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + return function(type) { + if (type == wanted) return cont(); + else if (wanted == ";") return pass(); + else return cont(arguments.callee); + }; + } + + function statement(type) { + if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "{") return cont(pushlex("}"), block, poplex); + if (type == ";") return cont(); + if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse); + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), + poplex, statement, poplex); + if (type == "variable") return cont(pushlex("stat"), maybelabel); + if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), + block, poplex, poplex); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), + statement, poplex, popcontext); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function expression(type) { + return expressionInner(type, false); + } + function expressionNoComma(type) { + return expressionInner(type, true); + } + function expressionInner(type, noComma) { + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef); + if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression); + if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type == "operator") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), commasep(expressionNoComma, "]"), poplex, maybeop); + if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeop); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + function maybeexpressionNoComma(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expressionNoComma); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(expression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "operator") { + if (/\+\+|--/.test(value)) return cont(me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == ";") return; + if (type == "(") return cont(pushlex(")", "call"), commasep(expressionNoComma, ")"), poplex, me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "variable") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + } else if (type == "number" || type == "string") { + cx.marked = type + " property"; + } + if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expressionNoComma); + } + function getterSetter(type) { + if (type == ":") return cont(expression); + if (type != "variable") return cont(expect(":"), expression); + cx.marked = "property"; + return cont(functiondef); + } + function commasep(what, end) { + function proceed(type) { + if (type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(what, proceed); + } + if (type == end) return cont(); + return cont(expect(end)); + } + return function(type) { + if (type == end) return cont(); + else return pass(what, proceed); + }; + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type) { + if (type == ":") return cont(typedef); + return pass(); + } + function typedef(type) { + if (type == "variable"){cx.marked = "variable-3"; return cont();} + return pass(); + } + function vardef1(type, value) { + if (type == "variable") { + register(value); + return isTS ? cont(maybetype, vardef2) : cont(vardef2); + } + return pass(); + } + function vardef2(type, value) { + if (value == "=") return cont(expressionNoComma, vardef2); + if (type == ",") return cont(vardef1); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef1, expect(";"), forspec2); + if (type == ";") return cont(forspec2); + if (type == "variable") return cont(formaybein); + return pass(expression, expect(";"), forspec2); + } + function formaybein(_type, value) { + if (value == "in") return cont(expression); + return cont(maybeoperatorComma, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in") return cont(expression); + return pass(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext); + } + function funarg(type, value) { + if (type == "variable") {register(value); return isTS ? cont(maybetype) : cont();} + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: jsTokenBase, + lastType: null, + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + globalVars: parserConfig.globalVars, + context: parserConfig.localVars && {vars: parserConfig.localVars}, + indented: 0 + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + } + if (state.tokenize != jsTokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == jsTokenComment) return CodeMirror.Pass; + if (state.tokenize != jsTokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; + // Kludge to prevent 'maybelse' from blocking lexical scope pops + for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse || /^else\b/.test(textAfter)) break; + } + if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? 4 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricChars: ":{}", + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + lineComment: jsonMode ? null : "//", + fold: "brace", + + helperType: jsonMode ? "json" : "javascript", + jsonMode: jsonMode + }; +}); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/test.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/test.js new file mode 100644 index 000000000..a2af527be --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/test.js @@ -0,0 +1,10 @@ +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "javascript"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + MT("locals", + "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] = [number 10]; [keyword return] [variable-2 a] + [variable-2 c] + [variable d]; }"); + + MT("comma-and-binop", + "[keyword function](){ [keyword var] [def x] = [number 1] + [number 2], [def y]; }"); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/typescript.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/typescript.html new file mode 100644 index 000000000..9cc5f4932 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/javascript/typescript.html @@ -0,0 +1,61 @@ + + +CodeMirror: TypeScript mode + + + + + + + + + +
+

TypeScript mode

+ + +
+ + + +

This is a specialization of the JavaScript mode.

+
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/index.html new file mode 100644 index 000000000..a6b541e2e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/index.html @@ -0,0 +1,359 @@ + + +CodeMirror: Markdown mode + + + + + + + + + + + +
+

Markdown mode

+
+ + + +

Optionally depends on the XML mode for properly highlighted inline XML blocks.

+ +

MIME types defined: text/x-markdown.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/markdown.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/markdown.js new file mode 100644 index 000000000..bf1750d5b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/markdown.js @@ -0,0 +1,551 @@ +CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { + + var htmlFound = CodeMirror.modes.hasOwnProperty("xml"); + var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? {name: "xml", htmlMode: true} : "text/plain"); + var aliases = { + html: "htmlmixed", + js: "javascript", + json: "application/json", + c: "text/x-csrc", + "c++": "text/x-c++src", + java: "text/x-java", + csharp: "text/x-csharp", + "c#": "text/x-csharp", + scala: "text/x-scala" + }; + + var getMode = (function () { + var i, modes = {}, mimes = {}, mime; + + var list = []; + for (var m in CodeMirror.modes) + if (CodeMirror.modes.propertyIsEnumerable(m)) list.push(m); + for (i = 0; i < list.length; i++) { + modes[list[i]] = list[i]; + } + var mimesList = []; + for (var m in CodeMirror.mimeModes) + if (CodeMirror.mimeModes.propertyIsEnumerable(m)) + mimesList.push({mime: m, mode: CodeMirror.mimeModes[m]}); + for (i = 0; i < mimesList.length; i++) { + mime = mimesList[i].mime; + mimes[mime] = mimesList[i].mime; + } + + for (var a in aliases) { + if (aliases[a] in modes || aliases[a] in mimes) + modes[a] = aliases[a]; + } + + return function (lang) { + return modes[lang] ? CodeMirror.getMode(cmCfg, modes[lang]) : null; + }; + }()); + + // Should underscores in words open/close em/strong? + if (modeCfg.underscoresBreakWords === undefined) + modeCfg.underscoresBreakWords = true; + + // Turn on fenced code blocks? ("```" to start/end) + if (modeCfg.fencedCodeBlocks === undefined) modeCfg.fencedCodeBlocks = false; + + // Turn on task lists? ("- [ ] " and "- [x] ") + if (modeCfg.taskLists === undefined) modeCfg.taskLists = false; + + var codeDepth = 0; + + var header = 'header' + , code = 'comment' + , quote1 = 'atom' + , quote2 = 'number' + , list1 = 'variable-2' + , list2 = 'variable-3' + , list3 = 'keyword' + , hr = 'hr' + , image = 'tag' + , linkinline = 'link' + , linkemail = 'link' + , linktext = 'link' + , linkhref = 'string' + , em = 'em' + , strong = 'strong'; + + var hrRE = /^([*\-=_])(?:\s*\1){2,}\s*$/ + , ulRE = /^[*\-+]\s+/ + , olRE = /^[0-9]+\.\s+/ + , taskListRE = /^\[(x| )\](?=\s)/ // Must follow ulRE or olRE + , headerRE = /^(?:\={1,}|-{1,})$/ + , textRE = /^[^!\[\]*_\\<>` "'(]+/; + + function switchInline(stream, state, f) { + state.f = state.inline = f; + return f(stream, state); + } + + function switchBlock(stream, state, f) { + state.f = state.block = f; + return f(stream, state); + } + + + // Blocks + + function blankLine(state) { + // Reset linkTitle state + state.linkTitle = false; + // Reset EM state + state.em = false; + // Reset STRONG state + state.strong = false; + // Reset state.quote + state.quote = 0; + if (!htmlFound && state.f == htmlBlock) { + state.f = inlineNormal; + state.block = blockNormal; + } + // Reset state.trailingSpace + state.trailingSpace = 0; + state.trailingSpaceNewLine = false; + // Mark this line as blank + state.thisLineHasContent = false; + return null; + } + + function blockNormal(stream, state) { + + var prevLineIsList = (state.list !== false); + if (state.list !== false && state.indentationDiff >= 0) { // Continued list + if (state.indentationDiff < 4) { // Only adjust indentation if *not* a code block + state.indentation -= state.indentationDiff; + } + state.list = null; + } else if (state.list !== false && state.indentation > 0) { + state.list = null; + state.listDepth = Math.floor(state.indentation / 4); + } else if (state.list !== false) { // No longer a list + state.list = false; + state.listDepth = 0; + } + + if (state.indentationDiff >= 4) { + state.indentation -= 4; + stream.skipToEnd(); + return code; + } else if (stream.eatSpace()) { + return null; + } else if (stream.peek() === '#' || (state.prevLineHasContent && stream.match(headerRE)) ) { + state.header = true; + } else if (stream.eat('>')) { + state.indentation++; + state.quote = 1; + stream.eatSpace(); + while (stream.eat('>')) { + stream.eatSpace(); + state.quote++; + } + } else if (stream.peek() === '[') { + return switchInline(stream, state, footnoteLink); + } else if (stream.match(hrRE, true)) { + return hr; + } else if ((!state.prevLineHasContent || prevLineIsList) && (stream.match(ulRE, true) || stream.match(olRE, true))) { + state.indentation += 4; + state.list = true; + state.listDepth++; + if (modeCfg.taskLists && stream.match(taskListRE, false)) { + state.taskList = true; + } + } else if (modeCfg.fencedCodeBlocks && stream.match(/^```([\w+#]*)/, true)) { + // try switching mode + state.localMode = getMode(RegExp.$1); + if (state.localMode) state.localState = state.localMode.startState(); + switchBlock(stream, state, local); + return code; + } + + return switchInline(stream, state, state.inline); + } + + function htmlBlock(stream, state) { + var style = htmlMode.token(stream, state.htmlState); + if (htmlFound && style === 'tag' && state.htmlState.type !== 'openTag' && !state.htmlState.context) { + state.f = inlineNormal; + state.block = blockNormal; + } + if (state.md_inside && stream.current().indexOf(">")!=-1) { + state.f = inlineNormal; + state.block = blockNormal; + state.htmlState.context = undefined; + } + return style; + } + + function local(stream, state) { + if (stream.sol() && stream.match(/^```/, true)) { + state.localMode = state.localState = null; + state.f = inlineNormal; + state.block = blockNormal; + return code; + } else if (state.localMode) { + return state.localMode.token(stream, state.localState); + } else { + stream.skipToEnd(); + return code; + } + } + + // Inline + function getType(state) { + var styles = []; + + if (state.taskOpen) { return "meta"; } + if (state.taskClosed) { return "property"; } + + if (state.strong) { styles.push(strong); } + if (state.em) { styles.push(em); } + + if (state.linkText) { styles.push(linktext); } + + if (state.code) { styles.push(code); } + + if (state.header) { styles.push(header); } + if (state.quote) { styles.push(state.quote % 2 ? quote1 : quote2); } + if (state.list !== false) { + var listMod = (state.listDepth - 1) % 3; + if (!listMod) { + styles.push(list1); + } else if (listMod === 1) { + styles.push(list2); + } else { + styles.push(list3); + } + } + + if (state.trailingSpaceNewLine) { + styles.push("trailing-space-new-line"); + } else if (state.trailingSpace) { + styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b")); + } + + return styles.length ? styles.join(' ') : null; + } + + function handleText(stream, state) { + if (stream.match(textRE, true)) { + return getType(state); + } + return undefined; + } + + function inlineNormal(stream, state) { + var style = state.text(stream, state); + if (typeof style !== 'undefined') + return style; + + if (state.list) { // List marker (*, +, -, 1., etc) + state.list = null; + return getType(state); + } + + if (state.taskList) { + var taskOpen = stream.match(taskListRE, true)[1] !== "x"; + if (taskOpen) state.taskOpen = true; + else state.taskClosed = true; + state.taskList = false; + return getType(state); + } + + state.taskOpen = false; + state.taskClosed = false; + + var ch = stream.next(); + + if (ch === '\\') { + stream.next(); + return getType(state); + } + + // Matches link titles present on next line + if (state.linkTitle) { + state.linkTitle = false; + var matchCh = ch; + if (ch === '(') { + matchCh = ')'; + } + matchCh = (matchCh+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh; + if (stream.match(new RegExp(regex), true)) { + return linkhref; + } + } + + // If this block is changed, it may need to be updated in GFM mode + if (ch === '`') { + var t = getType(state); + var before = stream.pos; + stream.eatWhile('`'); + var difference = 1 + stream.pos - before; + if (!state.code) { + codeDepth = difference; + state.code = true; + return getType(state); + } else { + if (difference === codeDepth) { // Must be exact + state.code = false; + return t; + } + return getType(state); + } + } else if (state.code) { + return getType(state); + } + + if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) { + stream.match(/\[[^\]]*\]/); + state.inline = state.f = linkHref; + return image; + } + + if (ch === '[' && stream.match(/.*\](\(| ?\[)/, false)) { + state.linkText = true; + return getType(state); + } + + if (ch === ']' && state.linkText) { + var type = getType(state); + state.linkText = false; + state.inline = state.f = linkHref; + return type; + } + + if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) { + return switchInline(stream, state, inlineElement(linkinline, '>')); + } + + if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) { + return switchInline(stream, state, inlineElement(linkemail, '>')); + } + + if (ch === '<' && stream.match(/^\w/, false)) { + if (stream.string.indexOf(">")!=-1) { + var atts = stream.string.substring(1,stream.string.indexOf(">")); + if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) { + state.md_inside = true; + } + } + stream.backUp(1); + return switchBlock(stream, state, htmlBlock); + } + + if (ch === '<' && stream.match(/^\/\w*?>/)) { + state.md_inside = false; + return "tag"; + } + + var ignoreUnderscore = false; + if (!modeCfg.underscoresBreakWords) { + if (ch === '_' && stream.peek() !== '_' && stream.match(/(\w)/, false)) { + var prevPos = stream.pos - 2; + if (prevPos >= 0) { + var prevCh = stream.string.charAt(prevPos); + if (prevCh !== '_' && prevCh.match(/(\w)/, false)) { + ignoreUnderscore = true; + } + } + } + } + var t = getType(state); + if (ch === '*' || (ch === '_' && !ignoreUnderscore)) { + if (state.strong === ch && stream.eat(ch)) { // Remove STRONG + state.strong = false; + return t; + } else if (!state.strong && stream.eat(ch)) { // Add STRONG + state.strong = ch; + return getType(state); + } else if (state.em === ch) { // Remove EM + state.em = false; + return t; + } else if (!state.em) { // Add EM + state.em = ch; + return getType(state); + } + } else if (ch === ' ') { + if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces + if (stream.peek() === ' ') { // Surrounded by spaces, ignore + return getType(state); + } else { // Not surrounded by spaces, back up pointer + stream.backUp(1); + } + } + } + + if (ch === ' ') { + if (stream.match(/ +$/, false)) { + state.trailingSpace++; + } else if (state.trailingSpace) { + state.trailingSpaceNewLine = true; + } + } + + return getType(state); + } + + function linkHref(stream, state) { + // Check if space, and return NULL if so (to avoid marking the space) + if(stream.eatSpace()){ + return null; + } + var ch = stream.next(); + if (ch === '(' || ch === '[') { + return switchInline(stream, state, inlineElement(linkhref, ch === '(' ? ')' : ']')); + } + return 'error'; + } + + function footnoteLink(stream, state) { + if (stream.match(/^[^\]]*\]:/, true)) { + state.f = footnoteUrl; + return linktext; + } + return switchInline(stream, state, inlineNormal); + } + + function footnoteUrl(stream, state) { + // Check if space, and return NULL if so (to avoid marking the space) + if(stream.eatSpace()){ + return null; + } + // Match URL + stream.match(/^[^\s]+/, true); + // Check for link title + if (stream.peek() === undefined) { // End of line, set flag to check next line + state.linkTitle = true; + } else { // More content on line, check if link title + stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true); + } + state.f = state.inline = inlineNormal; + return linkhref; + } + + var savedInlineRE = []; + function inlineRE(endChar) { + if (!savedInlineRE[endChar]) { + // Escape endChar for RegExp (taken from http://stackoverflow.com/a/494122/526741) + endChar = (endChar+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + // Match any non-endChar, escaped character, as well as the closing + // endChar. + savedInlineRE[endChar] = new RegExp('^(?:[^\\\\]|\\\\.)*?(' + endChar + ')'); + } + return savedInlineRE[endChar]; + } + + function inlineElement(type, endChar, next) { + next = next || inlineNormal; + return function(stream, state) { + stream.match(inlineRE(endChar)); + state.inline = state.f = next; + return type; + }; + } + + return { + startState: function() { + return { + f: blockNormal, + + prevLineHasContent: false, + thisLineHasContent: false, + + block: blockNormal, + htmlState: CodeMirror.startState(htmlMode), + indentation: 0, + + inline: inlineNormal, + text: handleText, + + linkText: false, + linkTitle: false, + em: false, + strong: false, + header: false, + taskList: false, + list: false, + listDepth: 0, + quote: 0, + trailingSpace: 0, + trailingSpaceNewLine: false + }; + }, + + copyState: function(s) { + return { + f: s.f, + + prevLineHasContent: s.prevLineHasContent, + thisLineHasContent: s.thisLineHasContent, + + block: s.block, + htmlState: CodeMirror.copyState(htmlMode, s.htmlState), + indentation: s.indentation, + + localMode: s.localMode, + localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null, + + inline: s.inline, + text: s.text, + linkTitle: s.linkTitle, + em: s.em, + strong: s.strong, + header: s.header, + taskList: s.taskList, + list: s.list, + listDepth: s.listDepth, + quote: s.quote, + trailingSpace: s.trailingSpace, + trailingSpaceNewLine: s.trailingSpaceNewLine, + md_inside: s.md_inside + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (stream.match(/^\s*$/, true)) { + state.prevLineHasContent = false; + return blankLine(state); + } else { + state.prevLineHasContent = state.thisLineHasContent; + state.thisLineHasContent = true; + } + + // Reset state.header + state.header = false; + + // Reset state.taskList + state.taskList = false; + + // Reset state.code + state.code = false; + + // Reset state.trailingSpace + state.trailingSpace = 0; + state.trailingSpaceNewLine = false; + + state.f = state.block; + var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length; + var difference = Math.floor((indentation - state.indentation) / 4) * 4; + if (difference > 4) difference = 4; + var adjustedIndentation = state.indentation + difference; + state.indentationDiff = adjustedIndentation - state.indentation; + state.indentation = adjustedIndentation; + if (indentation > 0) return null; + } + return state.f(stream, state); + }, + + blankLine: blankLine, + + getType: getType + }; + +}, "xml"); + +CodeMirror.defineMIME("text/x-markdown", "markdown"); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/test.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/test.js new file mode 100644 index 000000000..f16791728 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/markdown/test.js @@ -0,0 +1,656 @@ +(function() { + var mode = CodeMirror.getMode({tabSize: 4}, "markdown"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + MT("plainText", + "foo"); + + // Don't style single trailing space + MT("trailingSpace1", + "foo "); + + // Two or more trailing spaces should be styled with line break character + MT("trailingSpace2", + "foo[trailing-space-a ][trailing-space-new-line ]"); + + MT("trailingSpace3", + "foo[trailing-space-a ][trailing-space-b ][trailing-space-new-line ]"); + + MT("trailingSpace4", + "foo[trailing-space-a ][trailing-space-b ][trailing-space-a ][trailing-space-new-line ]"); + + // Code blocks using 4 spaces (regardless of CodeMirror.tabSize value) + MT("codeBlocksUsing4Spaces", + " [comment foo]"); + + // Code blocks using 4 spaces with internal indentation + MT("codeBlocksUsing4SpacesIndentation", + " [comment bar]", + " [comment hello]", + " [comment world]", + " [comment foo]", + "bar"); + + // Code blocks using 4 spaces with internal indentation + MT("codeBlocksUsing4SpacesIndentation", + " foo", + " [comment bar]", + " [comment hello]", + " [comment world]"); + + // Code blocks using 1 tab (regardless of CodeMirror.indentWithTabs value) + MT("codeBlocksUsing1Tab", + "\t[comment foo]"); + + // Inline code using backticks + MT("inlineCodeUsingBackticks", + "foo [comment `bar`]"); + + // Block code using single backtick (shouldn't work) + MT("blockCodeSingleBacktick", + "[comment `]", + "foo", + "[comment `]"); + + // Unclosed backticks + // Instead of simply marking as CODE, it would be nice to have an + // incomplete flag for CODE, that is styled slightly different. + MT("unclosedBackticks", + "foo [comment `bar]"); + + // Per documentation: "To include a literal backtick character within a + // code span, you can use multiple backticks as the opening and closing + // delimiters" + MT("doubleBackticks", + "[comment ``foo ` bar``]"); + + // Tests based on Dingus + // http://daringfireball.net/projects/markdown/dingus + // + // Multiple backticks within an inline code block + MT("consecutiveBackticks", + "[comment `foo```bar`]"); + + // Multiple backticks within an inline code block with a second code block + MT("consecutiveBackticks", + "[comment `foo```bar`] hello [comment `world`]"); + + // Unclosed with several different groups of backticks + MT("unclosedBackticks", + "[comment ``foo ``` bar` hello]"); + + // Closed with several different groups of backticks + MT("closedBackticks", + "[comment ``foo ``` bar` hello``] world"); + + // atx headers + // http://daringfireball.net/projects/markdown/syntax#header + + MT("atxH1", + "[header # foo]"); + + MT("atxH2", + "[header ## foo]"); + + MT("atxH3", + "[header ### foo]"); + + MT("atxH4", + "[header #### foo]"); + + MT("atxH5", + "[header ##### foo]"); + + MT("atxH6", + "[header ###### foo]"); + + // H6 - 7x '#' should still be H6, per Dingus + // http://daringfireball.net/projects/markdown/dingus + MT("atxH6NotH7", + "[header ####### foo]"); + + // Setext headers - H1, H2 + // Per documentation, "Any number of underlining =’s or -’s will work." + // http://daringfireball.net/projects/markdown/syntax#header + // Ideally, the text would be marked as `header` as well, but this is + // not really feasible at the moment. So, instead, we're testing against + // what works today, to avoid any regressions. + // + // Check if single underlining = works + MT("setextH1", + "foo", + "[header =]"); + + // Check if 3+ ='s work + MT("setextH1", + "foo", + "[header ===]"); + + // Check if single underlining - works + MT("setextH2", + "foo", + "[header -]"); + + // Check if 3+ -'s work + MT("setextH2", + "foo", + "[header ---]"); + + // Single-line blockquote with trailing space + MT("blockquoteSpace", + "[atom > foo]"); + + // Single-line blockquote + MT("blockquoteNoSpace", + "[atom >foo]"); + + // No blank line before blockquote + MT("blockquoteNoBlankLine", + "foo", + "[atom > bar]"); + + // Nested blockquote + MT("blockquoteSpace", + "[atom > foo]", + "[number > > foo]", + "[atom > > > foo]"); + + // Single-line blockquote followed by normal paragraph + MT("blockquoteThenParagraph", + "[atom >foo]", + "", + "bar"); + + // Multi-line blockquote (lazy mode) + MT("multiBlockquoteLazy", + "[atom >foo]", + "[atom bar]"); + + // Multi-line blockquote followed by normal paragraph (lazy mode) + MT("multiBlockquoteLazyThenParagraph", + "[atom >foo]", + "[atom bar]", + "", + "hello"); + + // Multi-line blockquote (non-lazy mode) + MT("multiBlockquote", + "[atom >foo]", + "[atom >bar]"); + + // Multi-line blockquote followed by normal paragraph (non-lazy mode) + MT("multiBlockquoteThenParagraph", + "[atom >foo]", + "[atom >bar]", + "", + "hello"); + + // Check list types + + MT("listAsterisk", + "foo", + "bar", + "", + "[variable-2 * foo]", + "[variable-2 * bar]"); + + MT("listPlus", + "foo", + "bar", + "", + "[variable-2 + foo]", + "[variable-2 + bar]"); + + MT("listDash", + "foo", + "bar", + "", + "[variable-2 - foo]", + "[variable-2 - bar]"); + + MT("listNumber", + "foo", + "bar", + "", + "[variable-2 1. foo]", + "[variable-2 2. bar]"); + + // Lists require a preceding blank line (per Dingus) + MT("listBogus", + "foo", + "1. bar", + "2. hello"); + + // Formatting in lists (*) + MT("listAsteriskFormatting", + "[variable-2 * ][variable-2&em *foo*][variable-2 bar]", + "[variable-2 * ][variable-2&strong **foo**][variable-2 bar]", + "[variable-2 * ][variable-2&strong **][variable-2&em&strong *foo**][variable-2&em *][variable-2 bar]", + "[variable-2 * ][variable-2&comment `foo`][variable-2 bar]"); + + // Formatting in lists (+) + MT("listPlusFormatting", + "[variable-2 + ][variable-2&em *foo*][variable-2 bar]", + "[variable-2 + ][variable-2&strong **foo**][variable-2 bar]", + "[variable-2 + ][variable-2&strong **][variable-2&em&strong *foo**][variable-2&em *][variable-2 bar]", + "[variable-2 + ][variable-2&comment `foo`][variable-2 bar]"); + + // Formatting in lists (-) + MT("listDashFormatting", + "[variable-2 - ][variable-2&em *foo*][variable-2 bar]", + "[variable-2 - ][variable-2&strong **foo**][variable-2 bar]", + "[variable-2 - ][variable-2&strong **][variable-2&em&strong *foo**][variable-2&em *][variable-2 bar]", + "[variable-2 - ][variable-2&comment `foo`][variable-2 bar]"); + + // Formatting in lists (1.) + MT("listNumberFormatting", + "[variable-2 1. ][variable-2&em *foo*][variable-2 bar]", + "[variable-2 2. ][variable-2&strong **foo**][variable-2 bar]", + "[variable-2 3. ][variable-2&strong **][variable-2&em&strong *foo**][variable-2&em *][variable-2 bar]", + "[variable-2 4. ][variable-2&comment `foo`][variable-2 bar]"); + + // Paragraph lists + MT("listParagraph", + "[variable-2 * foo]", + "", + "[variable-2 * bar]"); + + // Multi-paragraph lists + // + // 4 spaces + MT("listMultiParagraph", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + " [variable-2 hello]"); + + // 4 spaces, extra blank lines (should still be list, per Dingus) + MT("listMultiParagraphExtra", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + "", + " [variable-2 hello]"); + + // 4 spaces, plus 1 space (should still be list, per Dingus) + MT("listMultiParagraphExtraSpace", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + " [variable-2 hello]", + "", + " [variable-2 world]"); + + // 1 tab + MT("listTab", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + "\t[variable-2 hello]"); + + // No indent + MT("listNoIndent", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + "hello"); + + // Blockquote + MT("blockquote", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + " [variable-2&atom > hello]"); + + // Code block + MT("blockquoteCode", + "[variable-2 * foo]", + "", + "[variable-2 * bar]", + "", + " [comment > hello]", + "", + " [variable-2 world]"); + + // Code block followed by text + MT("blockquoteCodeText", + "[variable-2 * foo]", + "", + " [variable-2 bar]", + "", + " [comment hello]", + "", + " [variable-2 world]"); + + // Nested list + + MT("listAsteriskNested", + "[variable-2 * foo]", + "", + " [variable-3 * bar]"); + + MT("listPlusNested", + "[variable-2 + foo]", + "", + " [variable-3 + bar]"); + + MT("listDashNested", + "[variable-2 - foo]", + "", + " [variable-3 - bar]"); + + MT("listNumberNested", + "[variable-2 1. foo]", + "", + " [variable-3 2. bar]"); + + MT("listMixed", + "[variable-2 * foo]", + "", + " [variable-3 + bar]", + "", + " [keyword - hello]", + "", + " [variable-2 1. world]"); + + MT("listBlockquote", + "[variable-2 * foo]", + "", + " [variable-3 + bar]", + "", + " [atom&variable-3 > hello]"); + + MT("listCode", + "[variable-2 * foo]", + "", + " [variable-3 + bar]", + "", + " [comment hello]"); + + // Code with internal indentation + MT("listCodeIndentation", + "[variable-2 * foo]", + "", + " [comment bar]", + " [comment hello]", + " [comment world]", + " [comment foo]", + " [variable-2 bar]"); + + // List nesting edge cases + MT("listNested", + "[variable-2 * foo]", + "", + " [variable-3 * bar]", + "", + " [variable-2 hello]" + ); + MT("listNested", + "[variable-2 * foo]", + "", + " [variable-3 * bar]", + "", + " [variable-3 * foo]" + ); + + // Code followed by text + MT("listCodeText", + "[variable-2 * foo]", + "", + " [comment bar]", + "", + "hello"); + + // Following tests directly from official Markdown documentation + // http://daringfireball.net/projects/markdown/syntax#hr + + MT("hrSpace", + "[hr * * *]"); + + MT("hr", + "[hr ***]"); + + MT("hrLong", + "[hr *****]"); + + MT("hrSpaceDash", + "[hr - - -]"); + + MT("hrDashLong", + "[hr ---------------------------------------]"); + + // Inline link with title + MT("linkTitle", + "[link [[foo]]][string (http://example.com/ \"bar\")] hello"); + + // Inline link without title + MT("linkNoTitle", + "[link [[foo]]][string (http://example.com/)] bar"); + + // Inline link with image + MT("linkImage", + "[link [[][tag ![[foo]]][string (http://example.com/)][link ]]][string (http://example.com/)] bar"); + + // Inline link with Em + MT("linkEm", + "[link [[][link&em *foo*][link ]]][string (http://example.com/)] bar"); + + // Inline link with Strong + MT("linkStrong", + "[link [[][link&strong **foo**][link ]]][string (http://example.com/)] bar"); + + // Inline link with EmStrong + MT("linkEmStrong", + "[link [[][link&strong **][link&em&strong *foo**][link&em *][link ]]][string (http://example.com/)] bar"); + + // Image with title + MT("imageTitle", + "[tag ![[foo]]][string (http://example.com/ \"bar\")] hello"); + + // Image without title + MT("imageNoTitle", + "[tag ![[foo]]][string (http://example.com/)] bar"); + + // Image with asterisks + MT("imageAsterisks", + "[tag ![[*foo*]]][string (http://example.com/)] bar"); + + // Not a link. Should be normal text due to square brackets being used + // regularly in text, especially in quoted material, and no space is allowed + // between square brackets and parentheses (per Dingus). + MT("notALink", + "[[foo]] (bar)"); + + // Reference-style links + MT("linkReference", + "[link [[foo]]][string [[bar]]] hello"); + + // Reference-style links with Em + MT("linkReferenceEm", + "[link [[][link&em *foo*][link ]]][string [[bar]]] hello"); + + // Reference-style links with Strong + MT("linkReferenceStrong", + "[link [[][link&strong **foo**][link ]]][string [[bar]]] hello"); + + // Reference-style links with EmStrong + MT("linkReferenceEmStrong", + "[link [[][link&strong **][link&em&strong *foo**][link&em *][link ]]][string [[bar]]] hello"); + + // Reference-style links with optional space separator (per docuentation) + // "You can optionally use a space to separate the sets of brackets" + MT("linkReferenceSpace", + "[link [[foo]]] [string [[bar]]] hello"); + + // Should only allow a single space ("...use *a* space...") + MT("linkReferenceDoubleSpace", + "[[foo]] [[bar]] hello"); + + // Reference-style links with implicit link name + MT("linkImplicit", + "[link [[foo]]][string [[]]] hello"); + + // @todo It would be nice if, at some point, the document was actually + // checked to see if the referenced link exists + + // Link label, for reference-style links (taken from documentation) + + MT("labelNoTitle", + "[link [[foo]]:] [string http://example.com/]"); + + MT("labelIndented", + " [link [[foo]]:] [string http://example.com/]"); + + MT("labelSpaceTitle", + "[link [[foo bar]]:] [string http://example.com/ \"hello\"]"); + + MT("labelDoubleTitle", + "[link [[foo bar]]:] [string http://example.com/ \"hello\"] \"world\""); + + MT("labelTitleDoubleQuotes", + "[link [[foo]]:] [string http://example.com/ \"bar\"]"); + + MT("labelTitleSingleQuotes", + "[link [[foo]]:] [string http://example.com/ 'bar']"); + + MT("labelTitleParenthese", + "[link [[foo]]:] [string http://example.com/ (bar)]"); + + MT("labelTitleInvalid", + "[link [[foo]]:] [string http://example.com/] bar"); + + MT("labelLinkAngleBrackets", + "[link [[foo]]:] [string \"bar\"]"); + + MT("labelTitleNextDoubleQuotes", + "[link [[foo]]:] [string http://example.com/]", + "[string \"bar\"] hello"); + + MT("labelTitleNextSingleQuotes", + "[link [[foo]]:] [string http://example.com/]", + "[string 'bar'] hello"); + + MT("labelTitleNextParenthese", + "[link [[foo]]:] [string http://example.com/]", + "[string (bar)] hello"); + + MT("labelTitleNextMixed", + "[link [[foo]]:] [string http://example.com/]", + "(bar\" hello"); + + MT("linkWeb", + "[link ] foo"); + + MT("linkWebDouble", + "[link ] foo [link ]"); + + MT("linkEmail", + "[link ] foo"); + + MT("linkEmailDouble", + "[link ] foo [link ]"); + + MT("emAsterisk", + "[em *foo*] bar"); + + MT("emUnderscore", + "[em _foo_] bar"); + + MT("emInWordAsterisk", + "foo[em *bar*]hello"); + + MT("emInWordUnderscore", + "foo[em _bar_]hello"); + + // Per documentation: "...surround an * or _ with spaces, it’ll be + // treated as a literal asterisk or underscore." + + MT("emEscapedBySpaceIn", + "foo [em _bar _ hello_] world"); + + MT("emEscapedBySpaceOut", + "foo _ bar[em _hello_]world"); + + // Unclosed emphasis characters + // Instead of simply marking as EM / STRONG, it would be nice to have an + // incomplete flag for EM and STRONG, that is styled slightly different. + MT("emIncompleteAsterisk", + "foo [em *bar]"); + + MT("emIncompleteUnderscore", + "foo [em _bar]"); + + MT("strongAsterisk", + "[strong **foo**] bar"); + + MT("strongUnderscore", + "[strong __foo__] bar"); + + MT("emStrongAsterisk", + "[em *foo][em&strong **bar*][strong hello**] world"); + + MT("emStrongUnderscore", + "[em _foo][em&strong __bar_][strong hello__] world"); + + // "...same character must be used to open and close an emphasis span."" + MT("emStrongMixed", + "[em _foo][em&strong **bar*hello__ world]"); + + MT("emStrongMixed", + "[em *foo][em&strong __bar_hello** world]"); + + // These characters should be escaped: + // \ backslash + // ` backtick + // * asterisk + // _ underscore + // {} curly braces + // [] square brackets + // () parentheses + // # hash mark + // + plus sign + // - minus sign (hyphen) + // . dot + // ! exclamation mark + + MT("escapeBacktick", + "foo \\`bar\\`"); + + MT("doubleEscapeBacktick", + "foo \\\\[comment `bar\\\\`]"); + + MT("escapeAsterisk", + "foo \\*bar\\*"); + + MT("doubleEscapeAsterisk", + "foo \\\\[em *bar\\\\*]"); + + MT("escapeUnderscore", + "foo \\_bar\\_"); + + MT("doubleEscapeUnderscore", + "foo \\\\[em _bar\\\\_]"); + + MT("escapeHash", + "\\# foo"); + + MT("doubleEscapeHash", + "\\\\# foo"); + + + // Tests to make sure GFM-specific things aren't getting through + + MT("taskList", + "[variable-2 * [ ]] bar]"); + + MT("fencedCodeBlocks", + "[comment ```]", + "foo", + "[comment ```]"); +})(); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/meta.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/meta.js new file mode 100644 index 000000000..f6bbd1b45 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/meta.js @@ -0,0 +1,89 @@ +CodeMirror.modeInfo = [ + {name: 'APL', mime: 'text/apl', mode: 'apl'}, + {name: 'Asterisk', mime: 'text/x-asterisk', mode: 'asterisk'}, + {name: 'C', mime: 'text/x-csrc', mode: 'clike'}, + {name: 'C++', mime: 'text/x-c++src', mode: 'clike'}, + {name: 'Cobol', mime: 'text/x-cobol', mode: 'cobol'}, + {name: 'Java', mime: 'text/x-java', mode: 'clike'}, + {name: 'C#', mime: 'text/x-csharp', mode: 'clike'}, + {name: 'Scala', mime: 'text/x-scala', mode: 'clike'}, + {name: 'Clojure', mime: 'text/x-clojure', mode: 'clojure'}, + {name: 'CoffeeScript', mime: 'text/x-coffeescript', mode: 'coffeescript'}, + {name: 'Common Lisp', mime: 'text/x-common-lisp', mode: 'commonlisp'}, + {name: 'CSS', mime: 'text/css', mode: 'css'}, + {name: 'D', mime: 'text/x-d', mode: 'd'}, + {name: 'diff', mime: 'text/x-diff', mode: 'diff'}, + {name: 'DTD', mime: 'application/xml-dtd', mode: 'dtd'}, + {name: 'ECL', mime: 'text/x-ecl', mode: 'ecl'}, + {name: 'Eiffel', mime: 'text/x-eiffel', mode: 'eiffel'}, + {name: 'Erlang', mime: 'text/x-erlang', mode: 'erlang'}, + {name: 'Fortran', mime: 'text/x-fortran', mode: 'fortran'}, + {name: 'Gas', mime: 'text/x-gas', mode: 'gas'}, + {name: 'Gherkin', mime: 'text/x-feature', mode: 'gherkin'}, + {name: 'GitHub Flavored Markdown', mime: 'text/x-gfm', mode: 'gfm'}, + {name: 'Go', mime: 'text/x-go', mode: 'go'}, + {name: 'Groovy', mime: 'text/x-groovy', mode: 'groovy'}, + {name: 'HAML', mime: 'text/x-haml', mode: 'haml'}, + {name: 'Haskell', mime: 'text/x-haskell', mode: 'haskell'}, + {name: 'Haxe', mime: 'text/x-haxe', mode: 'haxe'}, + {name: 'ASP.NET', mime: 'application/x-aspx', mode: 'htmlembedded'}, + {name: 'Embedded Javascript', mime: 'application/x-ejs', mode: 'htmlembedded'}, + {name: 'JavaServer Pages', mime: 'application/x-jsp', mode: 'htmlembedded'}, + {name: 'HTML', mime: 'text/html', mode: 'htmlmixed'}, + {name: 'HTTP', mime: 'message/http', mode: 'http'}, + {name: 'Jade', mime: 'text/x-jade', mode: 'jade'}, + {name: 'JavaScript', mime: 'text/javascript', mode: 'javascript'}, + {name: 'JSON', mime: 'application/x-json', mode: 'javascript'}, + {name: 'JSON', mime: 'application/json', mode: 'javascript'}, + {name: 'TypeScript', mime: 'application/typescript', mode: 'javascript'}, + {name: 'Jinja2', mime: 'jinja2', mode: 'jinja2'}, + {name: 'LESS', mime: 'text/x-less', mode: 'less'}, + {name: 'LiveScript', mime: 'text/x-livescript', mode: 'livescript'}, + {name: 'Lua', mime: 'text/x-lua', mode: 'lua'}, + {name: 'Markdown (GitHub-flavour)', mime: 'text/x-markdown', mode: 'markdown'}, + {name: 'mIRC', mime: 'text/mirc', mode: 'mirc'}, + {name: 'Nginx', mime: 'text/x-nginx-conf', mode: 'nginx'}, + {name: 'NTriples', mime: 'text/n-triples', mode: 'ntriples'}, + {name: 'OCaml', mime: 'text/x-ocaml', mode: 'ocaml'}, + {name: 'Octave', mime: 'text/x-octave', mode: 'octave'}, + {name: 'Pascal', mime: 'text/x-pascal', mode: 'pascal'}, + {name: 'Perl', mime: 'text/x-perl', mode: 'perl'}, + {name: 'PHP', mime: 'text/x-php', mode: 'php'}, + {name: 'PHP(HTML)', mime: 'application/x-httpd-php', mode: 'php'}, + {name: 'Pig', mime: 'text/x-pig', mode: 'pig'}, + {name: 'Plain Text', mime: 'text/plain', mode: 'null'}, + {name: 'Properties files', mime: 'text/x-properties', mode: 'properties'}, + {name: 'Python', mime: 'text/x-python', mode: 'python'}, + {name: 'Cython', mime: 'text/x-cython', mode: 'python'}, + {name: 'R', mime: 'text/x-rsrc', mode: 'r'}, + {name: 'reStructuredText', mime: 'text/x-rst', mode: 'rst'}, + {name: 'Ruby', mime: 'text/x-ruby', mode: 'ruby'}, + {name: 'Rust', mime: 'text/x-rustsrc', mode: 'rust'}, + {name: 'Sass', mime: 'text/x-sass', mode: 'sass'}, + {name: 'Scheme', mime: 'text/x-scheme', mode: 'scheme'}, + {name: 'SCSS', mime: 'text/x-scss', mode: 'css'}, + {name: 'Shell', mime: 'text/x-sh', mode: 'shell'}, + {name: 'Sieve', mime: 'application/sieve', mode: 'sieve'}, + {name: 'Smalltalk', mime: 'text/x-stsrc', mode: 'smalltalk'}, + {name: 'Smarty', mime: 'text/x-smarty', mode: 'smarty'}, + {name: 'SmartyMixed', mime: 'text/x-smarty', mode: 'smartymixed'}, + {name: 'SPARQL', mime: 'application/x-sparql-query', mode: 'sparql'}, + {name: 'SQL', mime: 'text/x-sql', mode: 'sql'}, + {name: 'MariaDB', mime: 'text/x-mariadb', mode: 'sql'}, + {name: 'sTeX', mime: 'text/x-stex', mode: 'stex'}, + {name: 'LaTeX', mime: 'text/x-latex', mode: 'stex'}, + {name: 'Tcl', mime: 'text/x-tcl', mode: 'tcl'}, + {name: 'TiddlyWiki ', mime: 'text/x-tiddlywiki', mode: 'tiddlywiki'}, + {name: 'Tiki wiki', mime: 'text/tiki', mode: 'tiki'}, + {name: 'TOML', mime: 'text/x-toml', mode: 'toml'}, + {name: 'Turtle', mime: 'text/turtle', mode: 'turtle'}, + {name: 'VB.NET', mime: 'text/x-vb', mode: 'vb'}, + {name: 'VBScript', mime: 'text/vbscript', mode: 'vbscript'}, + {name: 'Velocity', mime: 'text/velocity', mode: 'velocity'}, + {name: 'Verilog', mime: 'text/x-verilog', mode: 'verilog'}, + {name: 'XML', mime: 'application/xml', mode: 'xml'}, + {name: 'HTML', mime: 'text/html', mode: 'xml'}, + {name: 'XQuery', mime: 'application/xquery', mode: 'xquery'}, + {name: 'YAML', mime: 'text/x-yaml', mode: 'yaml'}, + {name: 'Z80', mime: 'text/x-z80', mode: 'z80'} +]; diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/index.html b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/index.html new file mode 100644 index 000000000..873099aab --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/index.html @@ -0,0 +1,57 @@ + + +CodeMirror: XML mode + + + + + + + + + +
+

XML mode

+
+ +

The XML mode supports two configuration parameters:

+
+
htmlMode (boolean)
+
This switches the mode to parse HTML instead of XML. This + means attributes do not have to be quoted, and some elements + (such as br) do not require a closing tag.
+
alignCDATA (boolean)
+
Setting this to true will force the opening tag of CDATA + blocks to not be indented.
+
+ +

MIME types defined: application/xml, text/html.

+
diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/xml.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/xml.js new file mode 100644 index 000000000..4f49e07fa --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/mode/xml/xml.js @@ -0,0 +1,345 @@ +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag || true; + + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true + } : { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false + }; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var tagName, type; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + var isClose = stream.eat("/"); + tagName = ""; + var c; + while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + if (!tagName) return "tag error"; + type = isClose ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + var next = state.tokenize(stream, state); + return next ? next + " error" : "error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.eatWhile(/[^\s\u00a0=<>\"\']/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + var curState, curStream, setStyle; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + + function pushContext(tagName, startOfLine) { + var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent); + curState.context = { + prev: curState.context, + tagName: tagName, + indent: curState.indented, + startOfLine: startOfLine, + noIndent: noIndent + }; + } + function popContext() { + if (curState.context) curState.context = curState.context.prev; + } + + function element(type) { + if (type == "openTag") { + curState.tagName = tagName; + curState.tagStart = curStream.column(); + return cont(attributes, endtag(curState.startOfLine)); + } else if (type == "closeTag") { + var err = false; + if (curState.context) { + if (curState.context.tagName != tagName) { + if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) { + popContext(); + } + err = !curState.context || curState.context.tagName != tagName; + } + } else { + err = true; + } + if (err) setStyle = "error"; + return cont(endclosetag(err)); + } + return cont(); + } + function endtag(startOfLine) { + return function(type) { + var tagName = curState.tagName; + curState.tagName = curState.tagStart = null; + if (type == "selfcloseTag" || + (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase()))) { + maybePopContext(tagName.toLowerCase()); + return cont(); + } + if (type == "endTag") { + maybePopContext(tagName.toLowerCase()); + pushContext(tagName, startOfLine); + return cont(); + } + return cont(); + }; + } + function endclosetag(err) { + return function(type) { + if (err) setStyle = "error"; + if (type == "endTag") { popContext(); return cont(); } + setStyle = "error"; + return cont(arguments.callee); + }; + } + function maybePopContext(nextTagName) { + var parentTagName; + while (true) { + if (!curState.context) { + return; + } + parentTagName = curState.context.tagName.toLowerCase(); + if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || + !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(); + } + } + + function attributes(type) { + if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);} + if (type == "endTag" || type == "selfcloseTag") return pass(); + setStyle = "error"; + return cont(attributes); + } + function attribute(type) { + if (type == "equals") return cont(attvalue, attributes); + if (!Kludges.allowMissing) setStyle = "error"; + else if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);} + return (type == "endTag" || type == "selfcloseTag") ? pass() : cont(); + } + function attvalue(type) { + if (type == "string") return cont(attvaluemaybe); + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();} + setStyle = "error"; + return (type == "endTag" || type == "selfCloseTag") ? pass() : cont(); + } + function attvaluemaybe(type) { + if (type == "string") return cont(attvaluemaybe); + else return pass(); + } + + return { + startState: function() { + return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, tagStart: null, context: null}; + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) { + state.startOfLine = true; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + + setStyle = type = tagName = null; + var style = state.tokenize(stream, state); + state.type = type; + if ((style || type) && style != "comment") { + curState = state; curStream = stream; + while (true) { + var comb = state.cc.pop() || element; + if (comb(type || style)) break; + } + } + state.startOfLine = false; + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + return state.stringStartCol + 1; + } + if ((state.tokenize != inTag && state.tokenize != inText) || + context && context.noIndent) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (multilineTagIndentPastTag) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * multilineTagIndentFactor; + } + if (alignCDATA && /", + + configuration: parserConfig.htmlMode ? "html" : "xml", + helperType: parserConfig.htmlMode ? "html" : "xml" + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-day.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-day.css new file mode 100644 index 000000000..cbb9a4fa4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-day.css @@ -0,0 +1,34 @@ +/* + + Name: 3024 day + Author: Jan T. Sott (http://github.com/idleberg) + + CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + +*/ + +.cm-s-3024-day.CodeMirror {background: #f7f7f7; color: #3a3432;} +.cm-s-3024-day div.CodeMirror-selected {background: #d6d5d4 !important;} +.cm-s-3024-day .CodeMirror-gutters {background: #f7f7f7; border-right: 0px;} +.cm-s-3024-day .CodeMirror-linenumber {color: #807d7c;} +.cm-s-3024-day .CodeMirror-cursor {border-left: 1px solid #5c5855 !important;} + +.cm-s-3024-day span.cm-comment {color: #cdab53;} +.cm-s-3024-day span.cm-atom {color: #a16a94;} +.cm-s-3024-day span.cm-number {color: #a16a94;} + +.cm-s-3024-day span.cm-property, .cm-s-3024-day span.cm-attribute {color: #01a252;} +.cm-s-3024-day span.cm-keyword {color: #db2d20;} +.cm-s-3024-day span.cm-string {color: #fded02;} + +.cm-s-3024-day span.cm-variable {color: #01a252;} +.cm-s-3024-day span.cm-variable-2 {color: #01a0e4;} +.cm-s-3024-day span.cm-def {color: #e8bbd0;} +.cm-s-3024-day span.cm-bracket {color: #3a3432;} +.cm-s-3024-day span.cm-tag {color: #db2d20;} +.cm-s-3024-day span.cm-link {color: #a16a94;} +.cm-s-3024-day span.cm-error {background: #db2d20; color: #5c5855;} + +.cm-s-3024-day .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-3024-day .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-night.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-night.css new file mode 100644 index 000000000..2c62e221b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/3024-night.css @@ -0,0 +1,34 @@ +/* + + Name: 3024 night + Author: Jan T. Sott (http://github.com/idleberg) + + CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + +*/ + +.cm-s-3024-night.CodeMirror {background: #090300; color: #d6d5d4;} +.cm-s-3024-night div.CodeMirror-selected {background: #3a3432 !important;} +.cm-s-3024-night .CodeMirror-gutters {background: #090300; border-right: 0px;} +.cm-s-3024-night .CodeMirror-linenumber {color: #5c5855;} +.cm-s-3024-night .CodeMirror-cursor {border-left: 1px solid #807d7c !important;} + +.cm-s-3024-night span.cm-comment {color: #cdab53;} +.cm-s-3024-night span.cm-atom {color: #a16a94;} +.cm-s-3024-night span.cm-number {color: #a16a94;} + +.cm-s-3024-night span.cm-property, .cm-s-3024-night span.cm-attribute {color: #01a252;} +.cm-s-3024-night span.cm-keyword {color: #db2d20;} +.cm-s-3024-night span.cm-string {color: #fded02;} + +.cm-s-3024-night span.cm-variable {color: #01a252;} +.cm-s-3024-night span.cm-variable-2 {color: #01a0e4;} +.cm-s-3024-night span.cm-def {color: #e8bbd0;} +.cm-s-3024-night span.cm-bracket {color: #d6d5d4;} +.cm-s-3024-night span.cm-tag {color: #db2d20;} +.cm-s-3024-night span.cm-link {color: #a16a94;} +.cm-s-3024-night span.cm-error {background: #db2d20; color: #807d7c;} + +.cm-s-3024-night .CodeMirror-activeline-background {background: #2F2F2F !important;} +.cm-s-3024-night .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance-mobile.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance-mobile.css new file mode 100644 index 000000000..88d332e1a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance-mobile.css @@ -0,0 +1,5 @@ +.cm-s-ambiance.CodeMirror { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance.css new file mode 100644 index 000000000..a69d16e5a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/ambiance.css @@ -0,0 +1,75 @@ +/* ambiance theme for codemirror */ + +/* Color scheme */ + +.cm-s-ambiance .cm-keyword { color: #cda869; } +.cm-s-ambiance .cm-atom { color: #CF7EA9; } +.cm-s-ambiance .cm-number { color: #78CF8A; } +.cm-s-ambiance .cm-def { color: #aac6e3; } +.cm-s-ambiance .cm-variable { color: #ffb795; } +.cm-s-ambiance .cm-variable-2 { color: #eed1b3; } +.cm-s-ambiance .cm-variable-3 { color: #faded3; } +.cm-s-ambiance .cm-property { color: #eed1b3; } +.cm-s-ambiance .cm-operator {color: #fa8d6a;} +.cm-s-ambiance .cm-comment { color: #555; font-style:italic; } +.cm-s-ambiance .cm-string { color: #8f9d6a; } +.cm-s-ambiance .cm-string-2 { color: #9d937c; } +.cm-s-ambiance .cm-meta { color: #D2A8A1; } +.cm-s-ambiance .cm-qualifier { color: yellow; } +.cm-s-ambiance .cm-builtin { color: #9999cc; } +.cm-s-ambiance .cm-bracket { color: #24C2C7; } +.cm-s-ambiance .cm-tag { color: #fee4ff } +.cm-s-ambiance .cm-attribute { color: #9B859D; } +.cm-s-ambiance .cm-header {color: blue;} +.cm-s-ambiance .cm-quote { color: #24C2C7; } +.cm-s-ambiance .cm-hr { color: pink; } +.cm-s-ambiance .cm-link { color: #F4C20B; } +.cm-s-ambiance .cm-special { color: #FF9D00; } +.cm-s-ambiance .cm-error { color: #AF2018; } + +.cm-s-ambiance .CodeMirror-matchingbracket { color: #0f0; } +.cm-s-ambiance .CodeMirror-nonmatchingbracket { color: #f22; } + +.cm-s-ambiance .CodeMirror-selected { + background: rgba(255, 255, 255, 0.15); +} +.cm-s-ambiance .CodeMirror-focused .CodeMirror-selected { + background: rgba(255, 255, 255, 0.10); +} + +/* Editor styling */ + +.cm-s-ambiance.CodeMirror { + line-height: 1.40em; + font-family: Monaco, Menlo,"Andale Mono","lucida console","Courier New",monospace !important; + color: #E6E1DC; + background-color: #202020; + -webkit-box-shadow: inset 0 0 10px black; + -moz-box-shadow: inset 0 0 10px black; + box-shadow: inset 0 0 10px black; +} + +.cm-s-ambiance .CodeMirror-gutters { + background: #3D3D3D; + border-right: 1px solid #4D4D4D; + box-shadow: 0 10px 20px black; +} + +.cm-s-ambiance .CodeMirror-linenumber { + text-shadow: 0px 1px 1px #4d4d4d; + color: #222; + padding: 0 5px; +} + +.cm-s-ambiance .CodeMirror-lines .CodeMirror-cursor { + border-left: 1px solid #7991E8; +} + +.cm-s-ambiance .CodeMirror-activeline-background { + background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.031); +} + +.cm-s-ambiance.CodeMirror, +.cm-s-ambiance .CodeMirror-gutters { + background-image: url("data:image/png;base64,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"); +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-dark.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-dark.css new file mode 100644 index 000000000..3b7b21c7a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-dark.css @@ -0,0 +1,34 @@ +/* + + Name: Base16 Default Dark + Author: Chris Kempson (http://chriskempson.com) + + CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-chrome-devtools) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + +*/ + +.cm-s-base16-dark.CodeMirror {background: #151515; color: #e0e0e0;} +.cm-s-base16-dark div.CodeMirror-selected {background: #202020 !important;} +.cm-s-base16-dark .CodeMirror-gutters {background: #151515; border-right: 0px;} +.cm-s-base16-dark .CodeMirror-linenumber {color: #505050;} +.cm-s-base16-dark .CodeMirror-cursor {border-left: 1px solid #b0b0b0 !important;} + +.cm-s-base16-dark span.cm-comment {color: #8f5536;} +.cm-s-base16-dark span.cm-atom {color: #aa759f;} +.cm-s-base16-dark span.cm-number {color: #aa759f;} + +.cm-s-base16-dark span.cm-property, .cm-s-base16-dark span.cm-attribute {color: #90a959;} +.cm-s-base16-dark span.cm-keyword {color: #ac4142;} +.cm-s-base16-dark span.cm-string {color: #f4bf75;} + +.cm-s-base16-dark span.cm-variable {color: #90a959;} +.cm-s-base16-dark span.cm-variable-2 {color: #6a9fb5;} +.cm-s-base16-dark span.cm-def {color: #d28445;} +.cm-s-base16-dark span.cm-bracket {color: #e0e0e0;} +.cm-s-base16-dark span.cm-tag {color: #ac4142;} +.cm-s-base16-dark span.cm-link {color: #aa759f;} +.cm-s-base16-dark span.cm-error {background: #ac4142; color: #b0b0b0;} + +.cm-s-base16-dark .CodeMirror-activeline-background {background: #2F2F2F !important;} +.cm-s-base16-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-light.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-light.css new file mode 100644 index 000000000..5aa4b5389 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/base16-light.css @@ -0,0 +1,34 @@ +/* + + Name: Base16 Default Light + Author: Chris Kempson (http://chriskempson.com) + + CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-chrome-devtools) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + +*/ + +.cm-s-base16-light.CodeMirror {background: #f5f5f5; color: #202020;} +.cm-s-base16-light div.CodeMirror-selected {background: #e0e0e0 !important;} +.cm-s-base16-light .CodeMirror-gutters {background: #f5f5f5; border-right: 0px;} +.cm-s-base16-light .CodeMirror-linenumber {color: #b0b0b0;} +.cm-s-base16-light .CodeMirror-cursor {border-left: 1px solid #505050 !important;} + +.cm-s-base16-light span.cm-comment {color: #8f5536;} +.cm-s-base16-light span.cm-atom {color: #aa759f;} +.cm-s-base16-light span.cm-number {color: #aa759f;} + +.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute {color: #90a959;} +.cm-s-base16-light span.cm-keyword {color: #ac4142;} +.cm-s-base16-light span.cm-string {color: #f4bf75;} + +.cm-s-base16-light span.cm-variable {color: #90a959;} +.cm-s-base16-light span.cm-variable-2 {color: #6a9fb5;} +.cm-s-base16-light span.cm-def {color: #d28445;} +.cm-s-base16-light span.cm-bracket {color: #202020;} +.cm-s-base16-light span.cm-tag {color: #ac4142;} +.cm-s-base16-light span.cm-link {color: #aa759f;} +.cm-s-base16-light span.cm-error {background: #ac4142; color: #505050;} + +.cm-s-base16-light .CodeMirror-activeline-background {background: #DDDCDC !important;} +.cm-s-base16-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/blackboard.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/blackboard.css new file mode 100644 index 000000000..8b7608472 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/blackboard.css @@ -0,0 +1,28 @@ +/* Port of TextMate's Blackboard theme */ + +.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; } +.cm-s-blackboard .CodeMirror-selected { background: #253B76 !important; } +.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; } +.cm-s-blackboard .CodeMirror-linenumber { color: #888; } +.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7 !important; } + +.cm-s-blackboard .cm-keyword { color: #FBDE2D; } +.cm-s-blackboard .cm-atom { color: #D8FA3C; } +.cm-s-blackboard .cm-number { color: #D8FA3C; } +.cm-s-blackboard .cm-def { color: #8DA6CE; } +.cm-s-blackboard .cm-variable { color: #FF6400; } +.cm-s-blackboard .cm-operator { color: #FBDE2D;} +.cm-s-blackboard .cm-comment { color: #AEAEAE; } +.cm-s-blackboard .cm-string { color: #61CE3C; } +.cm-s-blackboard .cm-string-2 { color: #61CE3C; } +.cm-s-blackboard .cm-meta { color: #D8FA3C; } +.cm-s-blackboard .cm-builtin { color: #8DA6CE; } +.cm-s-blackboard .cm-tag { color: #8DA6CE; } +.cm-s-blackboard .cm-attribute { color: #8DA6CE; } +.cm-s-blackboard .cm-header { color: #FF6400; } +.cm-s-blackboard .cm-hr { color: #AEAEAE; } +.cm-s-blackboard .cm-link { color: #8DA6CE; } +.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; } + +.cm-s-blackboard .CodeMirror-activeline-background {background: #3C3636 !important;} +.cm-s-blackboard .CodeMirror-matchingbracket {outline:1px solid grey;color:white !important} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/cobalt.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/cobalt.css new file mode 100644 index 000000000..b4a917736 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/cobalt.css @@ -0,0 +1,21 @@ +.cm-s-cobalt.CodeMirror { background: #002240; color: white; } +.cm-s-cobalt div.CodeMirror-selected { background: #b36539 !important; } +.cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } +.cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; } +.cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-cobalt span.cm-comment { color: #08f; } +.cm-s-cobalt span.cm-atom { color: #845dc4; } +.cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; } +.cm-s-cobalt span.cm-keyword { color: #ffee80; } +.cm-s-cobalt span.cm-string { color: #3ad900; } +.cm-s-cobalt span.cm-meta { color: #ff9d00; } +.cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; } +.cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def { color: white; } +.cm-s-cobalt span.cm-bracket { color: #d8d8d8; } +.cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; } +.cm-s-cobalt span.cm-link { color: #845dc4; } +.cm-s-cobalt span.cm-error { color: #9d1e15; } + +.cm-s-cobalt .CodeMirror-activeline-background {background: #002D57 !important;} +.cm-s-cobalt .CodeMirror-matchingbracket {outline:1px solid grey;color:white !important} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/eclipse.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/eclipse.css new file mode 100644 index 000000000..317218e3d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/eclipse.css @@ -0,0 +1,23 @@ +.cm-s-eclipse span.cm-meta {color: #FF1717;} +.cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } +.cm-s-eclipse span.cm-atom {color: #219;} +.cm-s-eclipse span.cm-number {color: #164;} +.cm-s-eclipse span.cm-def {color: #00f;} +.cm-s-eclipse span.cm-variable {color: black;} +.cm-s-eclipse span.cm-variable-2 {color: #0000C0;} +.cm-s-eclipse span.cm-variable-3 {color: #0000C0;} +.cm-s-eclipse span.cm-property {color: black;} +.cm-s-eclipse span.cm-operator {color: black;} +.cm-s-eclipse span.cm-comment {color: #3F7F5F;} +.cm-s-eclipse span.cm-string {color: #2A00FF;} +.cm-s-eclipse span.cm-string-2 {color: #f50;} +.cm-s-eclipse span.cm-qualifier {color: #555;} +.cm-s-eclipse span.cm-builtin {color: #30a;} +.cm-s-eclipse span.cm-bracket {color: #cc7;} +.cm-s-eclipse span.cm-tag {color: #170;} +.cm-s-eclipse span.cm-attribute {color: #00c;} +.cm-s-eclipse span.cm-link {color: #219;} +.cm-s-eclipse span.cm-error {color: #f00;} + +.cm-s-eclipse .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-eclipse .CodeMirror-matchingbracket {outline:1px solid grey; color:black !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/elegant.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/elegant.css new file mode 100644 index 000000000..dd7df7b73 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/elegant.css @@ -0,0 +1,13 @@ +.cm-s-elegant span.cm-number, .cm-s-elegant span.cm-string, .cm-s-elegant span.cm-atom {color: #762;} +.cm-s-elegant span.cm-comment {color: #262; font-style: italic; line-height: 1em;} +.cm-s-elegant span.cm-meta {color: #555; font-style: italic; line-height: 1em;} +.cm-s-elegant span.cm-variable {color: black;} +.cm-s-elegant span.cm-variable-2 {color: #b11;} +.cm-s-elegant span.cm-qualifier {color: #555;} +.cm-s-elegant span.cm-keyword {color: #730;} +.cm-s-elegant span.cm-builtin {color: #30a;} +.cm-s-elegant span.cm-link {color: #762;} +.cm-s-elegant span.cm-error {background-color: #fdd;} + +.cm-s-elegant .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-elegant .CodeMirror-matchingbracket {outline:1px solid grey; color:black !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/erlang-dark.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/erlang-dark.css new file mode 100644 index 000000000..db56b1084 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/erlang-dark.css @@ -0,0 +1,30 @@ +.cm-s-erlang-dark.CodeMirror { background: #002240; color: white; } +.cm-s-erlang-dark div.CodeMirror-selected { background: #b36539 !important; } +.cm-s-erlang-dark .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } +.cm-s-erlang-dark .CodeMirror-linenumber { color: #d0d0d0; } +.cm-s-erlang-dark .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-erlang-dark span.cm-atom { color: #f133f1; } +.cm-s-erlang-dark span.cm-attribute { color: #ff80e1; } +.cm-s-erlang-dark span.cm-bracket { color: #ff9d00; } +.cm-s-erlang-dark span.cm-builtin { color: #eaa; } +.cm-s-erlang-dark span.cm-comment { color: #77f; } +.cm-s-erlang-dark span.cm-def { color: #e7a; } +.cm-s-erlang-dark span.cm-keyword { color: #ffee80; } +.cm-s-erlang-dark span.cm-meta { color: #50fefe; } +.cm-s-erlang-dark span.cm-number { color: #ffd0d0; } +.cm-s-erlang-dark span.cm-operator { color: #d55; } +.cm-s-erlang-dark span.cm-property { color: #ccc; } +.cm-s-erlang-dark span.cm-qualifier { color: #ccc; } +.cm-s-erlang-dark span.cm-quote { color: #ccc; } +.cm-s-erlang-dark span.cm-special { color: #ffbbbb; } +.cm-s-erlang-dark span.cm-string { color: #3ad900; } +.cm-s-erlang-dark span.cm-string-2 { color: #ccc; } +.cm-s-erlang-dark span.cm-tag { color: #9effff; } +.cm-s-erlang-dark span.cm-variable { color: #50fe50; } +.cm-s-erlang-dark span.cm-variable-2 { color: #e0e; } +.cm-s-erlang-dark span.cm-variable-3 { color: #ccc; } +.cm-s-erlang-dark span.cm-error { color: #9d1e15; } + +.cm-s-erlang-dark .CodeMirror-activeline-background {background: #013461 !important;} +.cm-s-erlang-dark .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/lesser-dark.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/lesser-dark.css new file mode 100644 index 000000000..c32559663 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/lesser-dark.css @@ -0,0 +1,47 @@ +/* +http://lesscss.org/ dark theme +Ported to CodeMirror by Peter Kroon +*/ +.cm-s-lesser-dark { + line-height: 1.3em; +} +.cm-s-lesser-dark { + font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Monaco', Courier, monospace !important; +} + +.cm-s-lesser-dark.CodeMirror { background: #262626; color: #EBEFE7; text-shadow: 0 -1px 1px #262626; } +.cm-s-lesser-dark div.CodeMirror-selected {background: #45443B !important;} /* 33322B*/ +.cm-s-lesser-dark .CodeMirror-cursor { border-left: 1px solid white !important; } +.cm-s-lesser-dark pre { padding: 0 8px; }/*editable code holder*/ + +.cm-s-lesser-dark.CodeMirror span.CodeMirror-matchingbracket { color: #7EFC7E; }/*65FC65*/ + +.cm-s-lesser-dark .CodeMirror-gutters { background: #262626; border-right:1px solid #aaa; } +.cm-s-lesser-dark .CodeMirror-linenumber { color: #777; } + +.cm-s-lesser-dark span.cm-keyword { color: #599eff; } +.cm-s-lesser-dark span.cm-atom { color: #C2B470; } +.cm-s-lesser-dark span.cm-number { color: #B35E4D; } +.cm-s-lesser-dark span.cm-def {color: white;} +.cm-s-lesser-dark span.cm-variable { color:#D9BF8C; } +.cm-s-lesser-dark span.cm-variable-2 { color: #669199; } +.cm-s-lesser-dark span.cm-variable-3 { color: white; } +.cm-s-lesser-dark span.cm-property {color: #92A75C;} +.cm-s-lesser-dark span.cm-operator {color: #92A75C;} +.cm-s-lesser-dark span.cm-comment { color: #666; } +.cm-s-lesser-dark span.cm-string { color: #BCD279; } +.cm-s-lesser-dark span.cm-string-2 {color: #f50;} +.cm-s-lesser-dark span.cm-meta { color: #738C73; } +.cm-s-lesser-dark span.cm-qualifier {color: #555;} +.cm-s-lesser-dark span.cm-builtin { color: #ff9e59; } +.cm-s-lesser-dark span.cm-bracket { color: #EBEFE7; } +.cm-s-lesser-dark span.cm-tag { color: #669199; } +.cm-s-lesser-dark span.cm-attribute {color: #00c;} +.cm-s-lesser-dark span.cm-header {color: #a0a;} +.cm-s-lesser-dark span.cm-quote {color: #090;} +.cm-s-lesser-dark span.cm-hr {color: #999;} +.cm-s-lesser-dark span.cm-link {color: #00c;} +.cm-s-lesser-dark span.cm-error { color: #9d1e15; } + +.cm-s-lesser-dark .CodeMirror-activeline-background {background: #3C3A3A !important;} +.cm-s-lesser-dark .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/mbo.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/mbo.css new file mode 100644 index 000000000..f3250a731 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/mbo.css @@ -0,0 +1,35 @@ +/* Based on mbonaci's Brackets mbo theme */ + +.cm-s-mbo.CodeMirror {background: #2c2c2c; color: #ffffe9;} +.cm-s-mbo div.CodeMirror-selected {background: #716C62 !important;} +.cm-s-mbo .CodeMirror-gutters {background: #4e4e4e; border-right: 0px;} +.cm-s-mbo .CodeMirror-linenumber {color: #dadada;} +.cm-s-mbo .CodeMirror-cursor {border-left: 1px solid #ffffec !important;} + +.cm-s-mbo span.cm-comment {color: #95958a;} +.cm-s-mbo span.cm-atom {color: #00a8c6;} +.cm-s-mbo span.cm-number {color: #00a8c6;} + +.cm-s-mbo span.cm-property, .cm-s-mbo span.cm-attribute {color: #9ddfe9;} +.cm-s-mbo span.cm-keyword {color: #ffb928;} +.cm-s-mbo span.cm-string {color: #ffcf6c;} + +.cm-s-mbo span.cm-variable {color: #ffffec;} +.cm-s-mbo span.cm-variable-2 {color: #00a8c6;} +.cm-s-mbo span.cm-def {color: #ffffec;} +.cm-s-mbo span.cm-bracket {color: #fffffc; font-weight: bold;} +.cm-s-mbo span.cm-tag {color: #9ddfe9;} +.cm-s-mbo span.cm-link {color: #f54b07;} +.cm-s-mbo span.cm-error {background: #636363; color: #ffffec;} + +.cm-s-mbo .CodeMirror-activeline-background {background: #494b41 !important;} +.cm-s-mbo .CodeMirror-matchingbracket { + text-decoration: underline; + color: #f5e107 !important; + } + +div.CodeMirror span.CodeMirror-searching { + background-color: none; + background: none; + box-shadow: 0 0 0 1px #ffffec; +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/midnight.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/midnight.css new file mode 100644 index 000000000..66126322b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/midnight.css @@ -0,0 +1,43 @@ +/* Based on the theme at http://bonsaiden.github.com/JavaScript-Garden */ + +/**/ +.cm-s-midnight span.CodeMirror-matchhighlight { background: #494949 } +.cm-s-midnight.CodeMirror-focused span.CodeMirror-matchhighlight { background: #314D67 !important; } + +/**/ +.cm-s-midnight .CodeMirror-activeline-background {background: #253540 !important;} + +.cm-s-midnight.CodeMirror { + background: #0F192A; + color: #D1EDFF; +} + +.cm-s-midnight.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;} + +.cm-s-midnight div.CodeMirror-selected {background: #314D67 !important;} +.cm-s-midnight .CodeMirror-gutters {background: #0F192A; border-right: 1px solid;} +.cm-s-midnight .CodeMirror-linenumber {color: #D0D0D0;} +.cm-s-midnight .CodeMirror-cursor { + border-left: 1px solid #F8F8F0 !important; +} + +.cm-s-midnight span.cm-comment {color: #428BDD;} +.cm-s-midnight span.cm-atom {color: #AE81FF;} +.cm-s-midnight span.cm-number {color: #D1EDFF;} + +.cm-s-midnight span.cm-property, .cm-s-midnight span.cm-attribute {color: #A6E22E;} +.cm-s-midnight span.cm-keyword {color: #E83737;} +.cm-s-midnight span.cm-string {color: #1DC116;} + +.cm-s-midnight span.cm-variable {color: #FFAA3E;} +.cm-s-midnight span.cm-variable-2 {color: #FFAA3E;} +.cm-s-midnight span.cm-def {color: #4DD;} +.cm-s-midnight span.cm-bracket {color: #D1EDFF;} +.cm-s-midnight span.cm-tag {color: #449;} +.cm-s-midnight span.cm-link {color: #AE81FF;} +.cm-s-midnight span.cm-error {background: #F92672; color: #F8F8F0;} + +.cm-s-midnight .CodeMirror-matchingbracket { + text-decoration: underline; + color: white !important; +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/monokai.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/monokai.css new file mode 100644 index 000000000..7ac601a13 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/monokai.css @@ -0,0 +1,29 @@ +/* Based on Sublime Text's Monokai theme */ + +.cm-s-monokai.CodeMirror {background: #272822; color: #f8f8f2;} +.cm-s-monokai div.CodeMirror-selected {background: #49483E !important;} +.cm-s-monokai .CodeMirror-gutters {background: #272822; border-right: 0px;} +.cm-s-monokai .CodeMirror-linenumber {color: #d0d0d0;} +.cm-s-monokai .CodeMirror-cursor {border-left: 1px solid #f8f8f0 !important;} + +.cm-s-monokai span.cm-comment {color: #75715e;} +.cm-s-monokai span.cm-atom {color: #ae81ff;} +.cm-s-monokai span.cm-number {color: #ae81ff;} + +.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {color: #a6e22e;} +.cm-s-monokai span.cm-keyword {color: #f92672;} +.cm-s-monokai span.cm-string {color: #e6db74;} + +.cm-s-monokai span.cm-variable {color: #a6e22e;} +.cm-s-monokai span.cm-variable-2 {color: #9effff;} +.cm-s-monokai span.cm-def {color: #fd971f;} +.cm-s-monokai span.cm-bracket {color: #f8f8f2;} +.cm-s-monokai span.cm-tag {color: #f92672;} +.cm-s-monokai span.cm-link {color: #ae81ff;} +.cm-s-monokai span.cm-error {background: #f92672; color: #f8f8f0;} + +.cm-s-monokai .CodeMirror-activeline-background {background: #373831 !important;} +.cm-s-monokai .CodeMirror-matchingbracket { + text-decoration: underline; + color: white !important; +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/neat.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/neat.css new file mode 100644 index 000000000..115083b81 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/neat.css @@ -0,0 +1,12 @@ +.cm-s-neat span.cm-comment { color: #a86; } +.cm-s-neat span.cm-keyword { line-height: 1em; font-weight: bold; color: blue; } +.cm-s-neat span.cm-string { color: #a22; } +.cm-s-neat span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; } +.cm-s-neat span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; } +.cm-s-neat span.cm-variable { color: black; } +.cm-s-neat span.cm-number, .cm-s-neat span.cm-atom { color: #3a3; } +.cm-s-neat span.cm-meta {color: #555;} +.cm-s-neat span.cm-link { color: #3a3; } + +.cm-s-neat .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-neat .CodeMirror-matchingbracket {outline:1px solid grey; color:black !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/night.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/night.css new file mode 100644 index 000000000..016e55ee7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/night.css @@ -0,0 +1,24 @@ +/* Loosely based on the Midnight Textmate theme */ + +.cm-s-night.CodeMirror { background: #0a001f; color: #f8f8f8; } +.cm-s-night div.CodeMirror-selected { background: #447 !important; } +.cm-s-night .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } +.cm-s-night .CodeMirror-linenumber { color: #f8f8f8; } +.cm-s-night .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-night span.cm-comment { color: #6900a1; } +.cm-s-night span.cm-atom { color: #845dc4; } +.cm-s-night span.cm-number, .cm-s-night span.cm-attribute { color: #ffd500; } +.cm-s-night span.cm-keyword { color: #599eff; } +.cm-s-night span.cm-string { color: #37f14a; } +.cm-s-night span.cm-meta { color: #7678e2; } +.cm-s-night span.cm-variable-2, .cm-s-night span.cm-tag { color: #99b2ff; } +.cm-s-night span.cm-variable-3, .cm-s-night span.cm-def { color: white; } +.cm-s-night span.cm-bracket { color: #8da6ce; } +.cm-s-night span.cm-comment { color: #6900a1; } +.cm-s-night span.cm-builtin, .cm-s-night span.cm-special { color: #ff9e59; } +.cm-s-night span.cm-link { color: #845dc4; } +.cm-s-night span.cm-error { color: #9d1e15; } + +.cm-s-night .CodeMirror-activeline-background {background: #1C005A !important;} +.cm-s-night .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-dark.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-dark.css new file mode 100644 index 000000000..ddefc55d2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-dark.css @@ -0,0 +1,34 @@ +/* + + Name: Paraíso (Dark) + Author: Jan T. Sott + + Color scheme by Jan T. Sott (https://github.com/idleberg/Paraiso-CodeMirror) + Inspired by the art of Rubens LP (http://www.rubenslp.com.br) + +*/ + +.cm-s-paraiso-dark.CodeMirror {background: #2f1e2e; color: #b9b6b0;} +.cm-s-paraiso-dark div.CodeMirror-selected {background: #41323f !important;} +.cm-s-paraiso-dark .CodeMirror-gutters {background: #2f1e2e; border-right: 0px;} +.cm-s-paraiso-dark .CodeMirror-linenumber {color: #776e71;} +.cm-s-paraiso-dark .CodeMirror-cursor {border-left: 1px solid #8d8687 !important;} + +.cm-s-paraiso-dark span.cm-comment {color: #e96ba8;} +.cm-s-paraiso-dark span.cm-atom {color: #815ba4;} +.cm-s-paraiso-dark span.cm-number {color: #815ba4;} + +.cm-s-paraiso-dark span.cm-property, .cm-s-paraiso-dark span.cm-attribute {color: #48b685;} +.cm-s-paraiso-dark span.cm-keyword {color: #ef6155;} +.cm-s-paraiso-dark span.cm-string {color: #fec418;} + +.cm-s-paraiso-dark span.cm-variable {color: #48b685;} +.cm-s-paraiso-dark span.cm-variable-2 {color: #06b6ef;} +.cm-s-paraiso-dark span.cm-def {color: #f99b15;} +.cm-s-paraiso-dark span.cm-bracket {color: #b9b6b0;} +.cm-s-paraiso-dark span.cm-tag {color: #ef6155;} +.cm-s-paraiso-dark span.cm-link {color: #815ba4;} +.cm-s-paraiso-dark span.cm-error {background: #ef6155; color: #8d8687;} + +.cm-s-paraiso-dark .CodeMirror-activeline-background {background: #4D344A !important;} +.cm-s-paraiso-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-light.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-light.css new file mode 100644 index 000000000..8afb14be4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/paraiso-light.css @@ -0,0 +1,34 @@ +/* + + Name: Paraíso (Light) + Author: Jan T. Sott + + Color scheme by Jan T. Sott (https://github.com/idleberg/Paraiso-CodeMirror) + Inspired by the art of Rubens LP (http://www.rubenslp.com.br) + +*/ + +.cm-s-paraiso-light.CodeMirror {background: #e7e9db; color: #41323f;} +.cm-s-paraiso-light div.CodeMirror-selected {background: #b9b6b0 !important;} +.cm-s-paraiso-light .CodeMirror-gutters {background: #e7e9db; border-right: 0px;} +.cm-s-paraiso-light .CodeMirror-linenumber {color: #8d8687;} +.cm-s-paraiso-light .CodeMirror-cursor {border-left: 1px solid #776e71 !important;} + +.cm-s-paraiso-light span.cm-comment {color: #e96ba8;} +.cm-s-paraiso-light span.cm-atom {color: #815ba4;} +.cm-s-paraiso-light span.cm-number {color: #815ba4;} + +.cm-s-paraiso-light span.cm-property, .cm-s-paraiso-light span.cm-attribute {color: #48b685;} +.cm-s-paraiso-light span.cm-keyword {color: #ef6155;} +.cm-s-paraiso-light span.cm-string {color: #fec418;} + +.cm-s-paraiso-light span.cm-variable {color: #48b685;} +.cm-s-paraiso-light span.cm-variable-2 {color: #06b6ef;} +.cm-s-paraiso-light span.cm-def {color: #f99b15;} +.cm-s-paraiso-light span.cm-bracket {color: #41323f;} +.cm-s-paraiso-light span.cm-tag {color: #ef6155;} +.cm-s-paraiso-light span.cm-link {color: #815ba4;} +.cm-s-paraiso-light span.cm-error {background: #ef6155; color: #776e71;} + +.cm-s-paraiso-light .CodeMirror-activeline-background {background: #CFD1C4 !important;} +.cm-s-paraiso-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/rubyblue.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/rubyblue.css new file mode 100644 index 000000000..b556139d7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/rubyblue.css @@ -0,0 +1,23 @@ +.cm-s-rubyblue { font-family: Trebuchet, Verdana, sans-serif; } /* - customized editor font - */ + +.cm-s-rubyblue.CodeMirror { background: #112435; color: white; } +.cm-s-rubyblue div.CodeMirror-selected { background: #38566F !important; } +.cm-s-rubyblue .CodeMirror-gutters { background: #1F4661; border-right: 7px solid #3E7087; } +.cm-s-rubyblue .CodeMirror-linenumber { color: white; } +.cm-s-rubyblue .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-rubyblue span.cm-comment { color: #999; font-style:italic; line-height: 1em; } +.cm-s-rubyblue span.cm-atom { color: #F4C20B; } +.cm-s-rubyblue span.cm-number, .cm-s-rubyblue span.cm-attribute { color: #82C6E0; } +.cm-s-rubyblue span.cm-keyword { color: #F0F; } +.cm-s-rubyblue span.cm-string { color: #F08047; } +.cm-s-rubyblue span.cm-meta { color: #F0F; } +.cm-s-rubyblue span.cm-variable-2, .cm-s-rubyblue span.cm-tag { color: #7BD827; } +.cm-s-rubyblue span.cm-variable-3, .cm-s-rubyblue span.cm-def { color: white; } +.cm-s-rubyblue span.cm-bracket { color: #F0F; } +.cm-s-rubyblue span.cm-link { color: #F4C20B; } +.cm-s-rubyblue span.CodeMirror-matchingbracket { color:#F0F !important; } +.cm-s-rubyblue span.cm-builtin, .cm-s-rubyblue span.cm-special { color: #FF9D00; } +.cm-s-rubyblue span.cm-error { color: #AF2018; } + +.cm-s-rubyblue .CodeMirror-activeline-background {background: #173047 !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/solarized.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/solarized.css new file mode 100644 index 000000000..af30d6213 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/solarized.css @@ -0,0 +1,180 @@ +/* +Solarized theme for code-mirror +http://ethanschoonover.com/solarized +*/ + +/* +Solarized color pallet +http://ethanschoonover.com/solarized/img/solarized-palette.png +*/ + +.solarized.base03 { color: #002b36; } +.solarized.base02 { color: #073642; } +.solarized.base01 { color: #586e75; } +.solarized.base00 { color: #657b83; } +.solarized.base0 { color: #839496; } +.solarized.base1 { color: #93a1a1; } +.solarized.base2 { color: #eee8d5; } +.solarized.base3 { color: #fdf6e3; } +.solarized.solar-yellow { color: #b58900; } +.solarized.solar-orange { color: #cb4b16; } +.solarized.solar-red { color: #dc322f; } +.solarized.solar-magenta { color: #d33682; } +.solarized.solar-violet { color: #6c71c4; } +.solarized.solar-blue { color: #268bd2; } +.solarized.solar-cyan { color: #2aa198; } +.solarized.solar-green { color: #859900; } + +/* Color scheme for code-mirror */ + +.cm-s-solarized { + line-height: 1.45em; + font-family: Menlo,Monaco,"Andale Mono","lucida console","Courier New",monospace !important; + color-profile: sRGB; + rendering-intent: auto; +} +.cm-s-solarized.cm-s-dark { + color: #839496; + background-color: #002b36; + text-shadow: #002b36 0 1px; +} +.cm-s-solarized.cm-s-light { + background-color: #fdf6e3; + color: #657b83; + text-shadow: #eee8d5 0 1px; +} + +.cm-s-solarized .CodeMirror-widget { + text-shadow: none; +} + + +.cm-s-solarized .cm-keyword { color: #cb4b16 } +.cm-s-solarized .cm-atom { color: #d33682; } +.cm-s-solarized .cm-number { color: #d33682; } +.cm-s-solarized .cm-def { color: #2aa198; } + +.cm-s-solarized .cm-variable { color: #268bd2; } +.cm-s-solarized .cm-variable-2 { color: #b58900; } +.cm-s-solarized .cm-variable-3 { color: #6c71c4; } + +.cm-s-solarized .cm-property { color: #2aa198; } +.cm-s-solarized .cm-operator {color: #6c71c4;} + +.cm-s-solarized .cm-comment { color: #586e75; font-style:italic; } + +.cm-s-solarized .cm-string { color: #859900; } +.cm-s-solarized .cm-string-2 { color: #b58900; } + +.cm-s-solarized .cm-meta { color: #859900; } +.cm-s-solarized .cm-qualifier { color: #b58900; } +.cm-s-solarized .cm-builtin { color: #d33682; } +.cm-s-solarized .cm-bracket { color: #cb4b16; } +.cm-s-solarized .CodeMirror-matchingbracket { color: #859900; } +.cm-s-solarized .CodeMirror-nonmatchingbracket { color: #dc322f; } +.cm-s-solarized .cm-tag { color: #93a1a1 } +.cm-s-solarized .cm-attribute { color: #2aa198; } +.cm-s-solarized .cm-header { color: #586e75; } +.cm-s-solarized .cm-quote { color: #93a1a1; } +.cm-s-solarized .cm-hr { + color: transparent; + border-top: 1px solid #586e75; + display: block; +} +.cm-s-solarized .cm-link { color: #93a1a1; cursor: pointer; } +.cm-s-solarized .cm-special { color: #6c71c4; } +.cm-s-solarized .cm-em { + color: #999; + text-decoration: underline; + text-decoration-style: dotted; +} +.cm-s-solarized .cm-strong { color: #eee; } +.cm-s-solarized .cm-tab:before { + content: "➤"; /*visualize tab character*/ + color: #586e75; +} +.cm-s-solarized .cm-error, +.cm-s-solarized .cm-invalidchar { + color: #586e75; + border-bottom: 1px dotted #dc322f; +} + +.cm-s-solarized.cm-s-dark .CodeMirror-selected { + background: #073642; +} + +.cm-s-solarized.cm-s-light .CodeMirror-selected { + background: #eee8d5; +} + +/* Editor styling */ + + + +/* Little shadow on the view-port of the buffer view */ +.cm-s-solarized.CodeMirror { + -moz-box-shadow: inset 7px 0 12px -6px #000; + -webkit-box-shadow: inset 7px 0 12px -6px #000; + box-shadow: inset 7px 0 12px -6px #000; +} + +/* Gutter border and some shadow from it */ +.cm-s-solarized .CodeMirror-gutters { + padding: 0 15px 0 10px; + box-shadow: 0 10px 20px black; + border-right: 1px solid; +} + +/* Gutter colors and line number styling based of color scheme (dark / light) */ + +/* Dark */ +.cm-s-solarized.cm-s-dark .CodeMirror-gutters { + background-color: #073642; + border-color: #00232c; +} + +.cm-s-solarized.cm-s-dark .CodeMirror-linenumber { + text-shadow: #021014 0 -1px; +} + +/* Light */ +.cm-s-solarized.cm-s-light .CodeMirror-gutters { + background-color: #eee8d5; + border-color: #eee8d5; +} + +/* Common */ +.cm-s-solarized .CodeMirror-linenumber { + color: #586e75; +} + +.cm-s-solarized .CodeMirror-gutter .CodeMirror-gutter-text { + color: #586e75; +} + +.cm-s-solarized .CodeMirror-lines { + padding-left: 5px; +} + +.cm-s-solarized .CodeMirror-lines .CodeMirror-cursor { + border-left: 1px solid #819090; +} + +/* +Active line. Negative margin compensates left padding of the text in the +view-port +*/ +.cm-s-solarized.cm-s-dark .CodeMirror-activeline-background { + background: rgba(255, 255, 255, 0.10); +} +.cm-s-solarized.cm-s-light .CodeMirror-activeline-background { + background: rgba(0, 0, 0, 0.10); +} + +/* +View-port and gutter both get little noise background to give it a real feel. +*/ +.cm-s-solarized.CodeMirror, +.cm-s-solarized .CodeMirror-gutters { + background-image: url("data:image/png;base64,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"); +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/the-matrix.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/the-matrix.css new file mode 100644 index 000000000..9e60b7b04 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/the-matrix.css @@ -0,0 +1,26 @@ +.cm-s-the-matrix.CodeMirror { background: #000000; color: #00FF00; } +.cm-s-the-matrix span.CodeMirror-selected { background: #a8f !important; } +.cm-s-the-matrix .CodeMirror-gutters { background: #060; border-right: 2px solid #00FF00; } +.cm-s-the-matrix .CodeMirror-linenumber { color: #FFFFFF; } +.cm-s-the-matrix .CodeMirror-cursor { border-left: 1px solid #00FF00 !important; } + +.cm-s-the-matrix span.cm-keyword {color: #008803; font-weight: bold;} +.cm-s-the-matrix span.cm-atom {color: #3FF;} +.cm-s-the-matrix span.cm-number {color: #FFB94F;} +.cm-s-the-matrix span.cm-def {color: #99C;} +.cm-s-the-matrix span.cm-variable {color: #F6C;} +.cm-s-the-matrix span.cm-variable-2 {color: #C6F;} +.cm-s-the-matrix span.cm-variable-3 {color: #96F;} +.cm-s-the-matrix span.cm-property {color: #62FFA0;} +.cm-s-the-matrix span.cm-operator {color: #999} +.cm-s-the-matrix span.cm-comment {color: #CCCCCC;} +.cm-s-the-matrix span.cm-string {color: #39C;} +.cm-s-the-matrix span.cm-meta {color: #C9F;} +.cm-s-the-matrix span.cm-qualifier {color: #FFF700;} +.cm-s-the-matrix span.cm-builtin {color: #30a;} +.cm-s-the-matrix span.cm-bracket {color: #cc7;} +.cm-s-the-matrix span.cm-tag {color: #FFBD40;} +.cm-s-the-matrix span.cm-attribute {color: #FFF700;} +.cm-s-the-matrix span.cm-error {color: #FF0000;} + +.cm-s-the-matrix .CodeMirror-activeline-background {background: #040;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/tomorrow-night-eighties.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/tomorrow-night-eighties.css new file mode 100644 index 000000000..85c2a4a78 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/tomorrow-night-eighties.css @@ -0,0 +1,34 @@ +/* + + Name: Tomorrow Night - Eighties + Author: Chris Kempson + + CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + +*/ + +.cm-s-tomorrow-night-eighties.CodeMirror {background: #000000; color: #CCCCCC;} +.cm-s-tomorrow-night-eighties div.CodeMirror-selected {background: #2D2D2D !important;} +.cm-s-tomorrow-night-eighties .CodeMirror-gutters {background: #000000; border-right: 0px;} +.cm-s-tomorrow-night-eighties .CodeMirror-linenumber {color: #515151;} +.cm-s-tomorrow-night-eighties .CodeMirror-cursor {border-left: 1px solid #6A6A6A !important;} + +.cm-s-tomorrow-night-eighties span.cm-comment {color: #d27b53;} +.cm-s-tomorrow-night-eighties span.cm-atom {color: #a16a94;} +.cm-s-tomorrow-night-eighties span.cm-number {color: #a16a94;} + +.cm-s-tomorrow-night-eighties span.cm-property, .cm-s-tomorrow-night-eighties span.cm-attribute {color: #99cc99;} +.cm-s-tomorrow-night-eighties span.cm-keyword {color: #f2777a;} +.cm-s-tomorrow-night-eighties span.cm-string {color: #ffcc66;} + +.cm-s-tomorrow-night-eighties span.cm-variable {color: #99cc99;} +.cm-s-tomorrow-night-eighties span.cm-variable-2 {color: #6699cc;} +.cm-s-tomorrow-night-eighties span.cm-def {color: #f99157;} +.cm-s-tomorrow-night-eighties span.cm-bracket {color: #CCCCCC;} +.cm-s-tomorrow-night-eighties span.cm-tag {color: #f2777a;} +.cm-s-tomorrow-night-eighties span.cm-link {color: #a16a94;} +.cm-s-tomorrow-night-eighties span.cm-error {background: #f2777a; color: #6A6A6A;} + +.cm-s-tomorrow-night-eighties .CodeMirror-activeline-background {background: #343600 !important;} +.cm-s-tomorrow-night-eighties .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/twilight.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/twilight.css new file mode 100644 index 000000000..19d6abadd --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/twilight.css @@ -0,0 +1,28 @@ +.cm-s-twilight.CodeMirror { background: #141414; color: #f7f7f7; } /**/ +.cm-s-twilight .CodeMirror-selected { background: #323232 !important; } /**/ + +.cm-s-twilight .CodeMirror-gutters { background: #222; border-right: 1px solid #aaa; } +.cm-s-twilight .CodeMirror-linenumber { color: #aaa; } +.cm-s-twilight .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-twilight .cm-keyword { color: #f9ee98; } /**/ +.cm-s-twilight .cm-atom { color: #FC0; } +.cm-s-twilight .cm-number { color: #ca7841; } /**/ +.cm-s-twilight .cm-def { color: #8DA6CE; } +.cm-s-twilight span.cm-variable-2, .cm-s-twilight span.cm-tag { color: #607392; } /**/ +.cm-s-twilight span.cm-variable-3, .cm-s-twilight span.cm-def { color: #607392; } /**/ +.cm-s-twilight .cm-operator { color: #cda869; } /**/ +.cm-s-twilight .cm-comment { color:#777; font-style:italic; font-weight:normal; } /**/ +.cm-s-twilight .cm-string { color:#8f9d6a; font-style:italic; } /**/ +.cm-s-twilight .cm-string-2 { color:#bd6b18 } /*?*/ +.cm-s-twilight .cm-meta { background-color:#141414; color:#f7f7f7; } /*?*/ +.cm-s-twilight .cm-builtin { color: #cda869; } /*?*/ +.cm-s-twilight .cm-tag { color: #997643; } /**/ +.cm-s-twilight .cm-attribute { color: #d6bb6d; } /*?*/ +.cm-s-twilight .cm-header { color: #FF6400; } +.cm-s-twilight .cm-hr { color: #AEAEAE; } +.cm-s-twilight .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } /**/ +.cm-s-twilight .cm-error { border-bottom: 1px solid red; } + +.cm-s-twilight .CodeMirror-activeline-background {background: #27282E !important;} +.cm-s-twilight .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/vibrant-ink.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/vibrant-ink.css new file mode 100644 index 000000000..0206225bf --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/vibrant-ink.css @@ -0,0 +1,30 @@ +/* Taken from the popular Visual Studio Vibrant Ink Schema */ + +.cm-s-vibrant-ink.CodeMirror { background: black; color: white; } +.cm-s-vibrant-ink .CodeMirror-selected { background: #35493c !important; } + +.cm-s-vibrant-ink .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } +.cm-s-vibrant-ink .CodeMirror-linenumber { color: #d0d0d0; } +.cm-s-vibrant-ink .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-vibrant-ink .cm-keyword { color: #CC7832; } +.cm-s-vibrant-ink .cm-atom { color: #FC0; } +.cm-s-vibrant-ink .cm-number { color: #FFEE98; } +.cm-s-vibrant-ink .cm-def { color: #8DA6CE; } +.cm-s-vibrant-ink span.cm-variable-2, .cm-s-vibrant span.cm-tag { color: #FFC66D } +.cm-s-vibrant-ink span.cm-variable-3, .cm-s-vibrant span.cm-def { color: #FFC66D } +.cm-s-vibrant-ink .cm-operator { color: #888; } +.cm-s-vibrant-ink .cm-comment { color: gray; font-weight: bold; } +.cm-s-vibrant-ink .cm-string { color: #A5C25C } +.cm-s-vibrant-ink .cm-string-2 { color: red } +.cm-s-vibrant-ink .cm-meta { color: #D8FA3C; } +.cm-s-vibrant-ink .cm-builtin { color: #8DA6CE; } +.cm-s-vibrant-ink .cm-tag { color: #8DA6CE; } +.cm-s-vibrant-ink .cm-attribute { color: #8DA6CE; } +.cm-s-vibrant-ink .cm-header { color: #FF6400; } +.cm-s-vibrant-ink .cm-hr { color: #AEAEAE; } +.cm-s-vibrant-ink .cm-link { color: blue; } +.cm-s-vibrant-ink .cm-error { border-bottom: 1px solid red; } + +.cm-s-vibrant-ink .CodeMirror-activeline-background {background: #27282E !important;} +.cm-s-vibrant-ink .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-dark.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-dark.css new file mode 100644 index 000000000..4a0b2138c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-dark.css @@ -0,0 +1,49 @@ +/* +Copyright (C) 2011 by MarkLogic Corporation +Author: Mike Brevoort + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +.cm-s-xq-dark.CodeMirror { background: #0a001f; color: #f8f8f8; } +.cm-s-xq-dark .CodeMirror-selected { background: #27007A !important; } +.cm-s-xq-dark .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } +.cm-s-xq-dark .CodeMirror-linenumber { color: #f8f8f8; } +.cm-s-xq-dark .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-xq-dark span.cm-keyword {color: #FFBD40;} +.cm-s-xq-dark span.cm-atom {color: #6C8CD5;} +.cm-s-xq-dark span.cm-number {color: #164;} +.cm-s-xq-dark span.cm-def {color: #FFF; text-decoration:underline;} +.cm-s-xq-dark span.cm-variable {color: #FFF;} +.cm-s-xq-dark span.cm-variable-2 {color: #EEE;} +.cm-s-xq-dark span.cm-variable-3 {color: #DDD;} +.cm-s-xq-dark span.cm-property {} +.cm-s-xq-dark span.cm-operator {} +.cm-s-xq-dark span.cm-comment {color: gray;} +.cm-s-xq-dark span.cm-string {color: #9FEE00;} +.cm-s-xq-dark span.cm-meta {color: yellow;} +.cm-s-xq-dark span.cm-qualifier {color: #FFF700;} +.cm-s-xq-dark span.cm-builtin {color: #30a;} +.cm-s-xq-dark span.cm-bracket {color: #cc7;} +.cm-s-xq-dark span.cm-tag {color: #FFBD40;} +.cm-s-xq-dark span.cm-attribute {color: #FFF700;} +.cm-s-xq-dark span.cm-error {color: #f00;} + +.cm-s-xq-dark .CodeMirror-activeline-background {background: #27282E !important;} +.cm-s-xq-dark .CodeMirror-matchingbracket {outline:1px solid grey; color:white !important;} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-light.css b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-light.css new file mode 100644 index 000000000..20b5c7961 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/codemirror/theme/xq-light.css @@ -0,0 +1,43 @@ +/* +Copyright (C) 2011 by MarkLogic Corporation +Author: Mike Brevoort + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +.cm-s-xq-light span.cm-keyword {line-height: 1em; font-weight: bold; color: #5A5CAD; } +.cm-s-xq-light span.cm-atom {color: #6C8CD5;} +.cm-s-xq-light span.cm-number {color: #164;} +.cm-s-xq-light span.cm-def {text-decoration:underline;} +.cm-s-xq-light span.cm-variable {color: black; } +.cm-s-xq-light span.cm-variable-2 {color:black;} +.cm-s-xq-light span.cm-variable-3 {color: black; } +.cm-s-xq-light span.cm-property {} +.cm-s-xq-light span.cm-operator {} +.cm-s-xq-light span.cm-comment {color: #0080FF; font-style: italic;} +.cm-s-xq-light span.cm-string {color: red;} +.cm-s-xq-light span.cm-meta {color: yellow;} +.cm-s-xq-light span.cm-qualifier {color: grey} +.cm-s-xq-light span.cm-builtin {color: #7EA656;} +.cm-s-xq-light span.cm-bracket {color: #cc7;} +.cm-s-xq-light span.cm-tag {color: #3F7F7F;} +.cm-s-xq-light span.cm-attribute {color: #7F007F;} +.cm-s-xq-light span.cm-error {color: #f00;} + +.cm-s-xq-light .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-xq-light .CodeMirror-matchingbracket {outline:1px solid grey;color:black !important;background:yellow;} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Scripts/template-editor.js b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/template-editor.js new file mode 100644 index 000000000..fb4dd5cfa --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Scripts/template-editor.js @@ -0,0 +1,19 @@ +(function ($) { + var initializeEditor = function () { + var textArea = $(".code-editor")[0]; + var editor = CodeMirror.fromTextArea(textArea, { + lineNumbers: true, + mode: "application/x-ejs", + indentUnit: 4, + indentWithTabs: true, + enterMode: "keep", + tabMode: "shift", + theme: "default", + autoCloseTags: true + }); + }; + + $(function () { + initializeEditor(); + }); +})(jQuery); \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/DefaultTemplateService.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/DefaultTemplateService.cs new file mode 100644 index 000000000..d0785ab91 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/DefaultTemplateService.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.DisplayManagement; +using Orchard.DisplayManagement.Implementation; +using Orchard.Mvc; +using Orchard.Templates.Models; + +namespace Orchard.Templates.Services { + public class DefaultTemplateService : ITemplateService { + + public const string TemplatesSignal = "Orchard.Templates"; + + private readonly IShapeFactory _shapeFactory; + private readonly IDisplayHelperFactory _displayHelperFactory; + private readonly IWorkContextAccessor _workContextAccessor; + private readonly IContentManager _contentManager; + private readonly IEnumerable _processors; + private readonly IHttpContextAccessor _httpContextAccessor; + + public DefaultTemplateService( + IShapeFactory shapeFactory, + IDisplayHelperFactory displayHelperFactory, + IWorkContextAccessor workContextAccessor, + IContentManager contentManager, + IEnumerable processors, + IHttpContextAccessor httpContextAccessor) { + _shapeFactory = shapeFactory; + _displayHelperFactory = displayHelperFactory; + _workContextAccessor = workContextAccessor; + _contentManager = contentManager; + _processors = processors; + _httpContextAccessor = httpContextAccessor; + } + + public string ExecuteShape(string shapeType) { + return ExecuteShape(shapeType, null); + } + + public string ExecuteShape(string shapeType, INamedEnumerable parameters) { + var shape = _shapeFactory.Create(shapeType, parameters); + var result = ""; + + ExecuteInHttpContext(httpContext => { + var viewContext = new ViewContext { HttpContext = httpContext }; + viewContext.RouteData.DataTokens["IWorkContextAccessor"] = _workContextAccessor; + var display = _displayHelperFactory.CreateHelper(viewContext, new ViewDataContainer()); + result = ((DisplayHelper)display).ShapeExecute(shape).ToString(); + }); + + return result; + } + + public string Execute(string template, string name, string language, TModel model = default(TModel)) { + return Execute(template, name, language, null, model); + } + + public string Execute(string template, string name, string language, DisplayContext context, TModel model = default(TModel)) { + var processor = _processors.FirstOrDefault(x => String.Equals(x.Type, language, StringComparison.OrdinalIgnoreCase)); + return processor != null ? processor.Process(template, name, context, model) : string.Empty; + } + + public IEnumerable GetTemplates(VersionOptions versionOptions = null) { + return _contentManager.Query(versionOptions ?? VersionOptions.Published).List(); + } + + /// + /// Executes the action in an HttpContext, regardless of whether or not we're in a ThreadStaticScope or HttpContextScope. + /// + private void ExecuteInHttpContext(Action action) { + var httpContext = _httpContextAccessor.Current(); + + if (httpContext != null) { + action(httpContext); + return; + } + + // We're not using the ViewContext.HttpContext because that will be an EmptyHttpContext when we're on a background thread. + // EmptyHttpContext does not implement Items, which is necessary for Orchard when storing and accessing certain services such as IWorkContextAccessor. + httpContext = new StubHttpContext(); + using (var scope = _workContextAccessor.CreateWorkContextScope(httpContext)) { + _httpContextAccessor.Set(httpContext); + action(httpContext); + } + } + + private class ViewDataContainer : IViewDataContainer { + public ViewDataDictionary ViewData { get; set; } + + public ViewDataContainer() { + ViewData = new ViewDataDictionary(); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateProcessor.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateProcessor.cs new file mode 100644 index 000000000..14b65b80f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateProcessor.cs @@ -0,0 +1,9 @@ +using Orchard.DisplayManagement.Implementation; + +namespace Orchard.Templates.Services { + public interface ITemplateProcessor : IDependency { + string Type { get; } + string Process(string template, string name, DisplayContext context = null, dynamic model = null); + void Verify(string template); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateService.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateService.cs new file mode 100644 index 000000000..04e71af00 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/ITemplateService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.DisplayManagement; +using Orchard.DisplayManagement.Implementation; +using Orchard.Templates.Models; + +namespace Orchard.Templates.Services { + public interface ITemplateService : IDependency { + string ExecuteShape(string shapeType); + string ExecuteShape(string shapeType, INamedEnumerable parameters); + string Execute(string template, string name, string language, TModel model = default(TModel)); + string Execute(string template, string name, string language, DisplayContext context, TModel model = default(TModel)); + IEnumerable GetTemplates(VersionOptions versionOptions = null); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs new file mode 100644 index 000000000..f17f3cf6a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Web.Mvc; +using System.Web.UI; +using System.Web.WebPages; +using Orchard.Compilation.Razor; +using Orchard.ContentManagement; +using Orchard.DisplayManagement.Implementation; +using Orchard.Logging; +using Orchard.Mvc; +using Orchard.Settings; +using Orchard.Themes.Models; + +namespace Orchard.Templates.Services { + public class RazorTemplateProcessor : TemplateProcessorImpl { + private readonly IRazorCompiler _compiler; + private readonly IRazorTemplateCache _cache; + private readonly ISiteService _siteService; + private readonly IHttpContextAccessor _hca; + + public override string Type { + get { return "Razor"; } + } + + public RazorTemplateProcessor(IRazorCompiler compiler, IRazorTemplateCache cache, ISiteService siteService, IHttpContextAccessor hca) { + _compiler = compiler; + _cache = cache; + _siteService = siteService; + _hca = hca; + Logger = NullLogger.Instance; + } + + ILogger Logger { get; set; } + + public override void Verify(string template) { + _compiler.CompileRazor(template, null, new Dictionary()); + } + + public override string Process(string template, string name, DisplayContext context = null, dynamic model = null) { + if (String.IsNullOrEmpty(template)) + return string.Empty; + + var compiledTemplate = _compiler.CompileRazor(template, name, new Dictionary()); + var result = ActivateAndRenderTemplate(compiledTemplate, context, null, model); + return result; + } + + private string ActivateAndRenderTemplate(IRazorTemplateBase obj, DisplayContext displayContext, string templateVirtualPath, object model) { + var buffer = new StringBuilder(1024); + using (var writer = new StringWriter(buffer)) { + var htmlWriter = new HtmlTextWriter(writer); + var httpContext = _hca.Current(); + + // this should be done better - if no display context is provided we should fallback to current controller context somehow, if possible + if (displayContext != null) { + var shapeViewContext = new ViewContext(displayContext.ViewContext.Controller.ControllerContext, displayContext.ViewContext.View, displayContext.ViewContext.ViewData, displayContext.ViewContext.TempData, htmlWriter); + obj.WebPageContext = new WebPageContext(displayContext.ViewContext.HttpContext, obj as WebPageRenderingBase, model); + obj.ViewContext = shapeViewContext; + obj.ViewData = new ViewDataDictionary(displayContext.ViewDataContainer.ViewData) { Model = model }; + } + else { + obj.ViewData = new ViewDataDictionary(model); + obj.WebPageContext = new WebPageContext(httpContext, obj as WebPageRenderingBase, model); + } + + obj.VirtualPath = templateVirtualPath ?? "~/Themes/" + _siteService.GetSiteSettings().As().CurrentThemeName; + obj.InitHelpers(); + obj.Render(htmlWriter); + } + + return buffer.ToString(); + } + } + +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/StubHttpContext.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/StubHttpContext.cs new file mode 100644 index 000000000..bee75d6e0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/StubHttpContext.cs @@ -0,0 +1,101 @@ +using System.Collections; +using System.Collections.Specialized; +using System.Security.Principal; +using System.Web; +using System.Web.ClientServices; +using System.Web.Routing; +using System.Web.Security; + +namespace Orchard.Templates.Services { + internal class StubHttpContext : HttpContextBase { + private IDictionary _items; + private HttpRequestBase _request; + private HttpSessionStateBase _session; + private IPrincipal _user; + + public override IDictionary Items { + get { return _items ?? (_items = new Hashtable()); } + } + + public override HttpRequestBase Request { + get { return _request ?? (_request = new StubHttpRequest(this)); } + } + + public override HttpSessionStateBase Session { + get { return _session ?? (_session = new StubHttpSessionState()); } + } + + public override IPrincipal User { + get { return _user ?? (_user = new ClientRolePrincipal(new FormsIdentity(new FormsAuthenticationTicket(FormsAuthentication.FormsCookieName, true, int.MaxValue)))); } + set { _user = value; } + } + + public override IHttpHandler Handler { get; set; } + + private class StubHttpRequest : HttpRequestBase { + private readonly HttpContextBase _httpContext; + private RequestContext _requestContext; + private NameValueCollection _queryString; + private NameValueCollection _headers; + + public StubHttpRequest(HttpContextBase httpContext) { + _httpContext = httpContext; + } + + public override RequestContext RequestContext { + get { return _requestContext ?? (_requestContext = new StubRequestContext(_httpContext)); } + set { _requestContext = value; } + } + + public override NameValueCollection QueryString { + get { return _queryString ?? (_queryString = new NameValueCollection()); } + } + + public override NameValueCollection Headers { + get { return _headers ?? (_headers = new NameValueCollection()); } + } + + public override bool IsAuthenticated { + get { return true; } + } + + public override string AppRelativeCurrentExecutionFilePath { + get { return "/"; } + } + } + + private class StubRequestContext : RequestContext { + private HttpContextBase _httpContext; + + public StubRequestContext(HttpContextBase httpContext) { + _httpContext = httpContext; + } + + public override HttpContextBase HttpContext { + get { return _httpContext; } + set { _httpContext = value; } + } + } + + private class StubHttpSessionState : HttpSessionStateBase { + private readonly Hashtable _stub = new Hashtable(); + public override object this[int index] { + get { return _stub[index]; } + set { _stub[index] = value; } + } + + public override object this[string index] { + get { return _stub[index]; } + set { _stub[index] = value; } + } + + public override int Count { + get { return _stub.Count; } + } + + public override void Clear() { + _stub.Clear(); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateBindingStrategy.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateBindingStrategy.cs new file mode 100644 index 000000000..9cfc4b0b8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateBindingStrategy.cs @@ -0,0 +1,136 @@ +using System; +using System.Linq; +using System.Web; +using Orchard.Caching; +using Orchard.Compilation.Razor; +using Orchard.ContentManagement; +using Orchard.DisplayManagement.Descriptors; +using Orchard.DisplayManagement.Implementation; +using Orchard.Environment.Extensions; +using Orchard.Environment.Extensions.Models; +using Orchard.Mvc.Spooling; +using Orchard.Settings; +using Orchard.Themes.Models; + +namespace Orchard.Templates.Services { + public class TemplateBindingStrategy : IShapeTableProvider { + private readonly IWorkContextAccessor _wca; + private readonly ICacheManager _cache; + private readonly ISignals _signals; + + public TemplateBindingStrategy(IWorkContextAccessor wca, ICacheManager cache, ISignals signals) { + _wca = wca; + _cache = cache; + _signals = signals; + } + + public void Discover(ShapeTableBuilder builder) { + EnsureWorkContext(() => BuildShapes(builder)); + } + + private void BuildShapes(ShapeTableBuilder builder) { + + var templateService = _wca.GetContext().Resolve(); + var templateCache = _wca.GetContext().Resolve(); + var siteService = _wca.GetContext().Resolve(); + var extensionManager = _wca.GetContext().Resolve(); + + var currentTheme = extensionManager.GetExtension(siteService.GetSiteSettings().As().CurrentThemeName); + var themeFeature = currentTheme.Features.FirstOrDefault(); + + var hackedDescriptor = new FeatureDescriptor + { + Category = themeFeature.Category, + Dependencies = themeFeature.Dependencies, + Description = themeFeature.Description, + Extension = themeFeature.Extension, + Id = themeFeature.Id, + Name = themeFeature.Name, + Priority = int.MaxValue + }; + + var shapes = _cache.Get( + DefaultTemplateService.TemplatesSignal, + ctx => { + ctx.Monitor(_signals.When(DefaultTemplateService.TemplatesSignal)); + return templateService + .GetTemplates() + .Select(r => new { + r.Name, + r.Language, + r.Template }) + .ToList(); + }); + + foreach (var record in shapes) { + templateCache.Set(record.Name, record.Template); + var shapeType = AdjustName(record.Name); + + builder.Describe(shapeType) + .From(new Feature { Descriptor = hackedDescriptor }) + .BoundAs("Template::" + shapeType, + descriptor => context => { + var template = templateCache.Get(record.Name); + return template != null ? PerformInvoke(context, record.Name, record.Language, template) : new HtmlString(""); + }); + } + } + + private IHtmlString PerformInvoke(DisplayContext displayContext, string name, string type, string template) + { + var service = _wca.GetContext().Resolve(); + var output = new HtmlStringWriter(); + + if (String.IsNullOrEmpty(template)) + return null; + + output.Write(CoerceHtmlString(service.Execute(template, name, type, displayContext, displayContext.Value))); + + return output; + } + + private string AdjustName(string name) { + var lastDash = name.LastIndexOf('-'); + var lastDot = name.LastIndexOf('.'); + if (lastDot <= 0 || lastDot < lastDash) { + name = Adjust(name, null); + return name; + } + + var displayType = name.Substring(lastDot + 1); + name = Adjust(name.Substring(0, lastDot), displayType); + return name; + } + + private static string Adjust(string name, string displayType) { + // Canonical shape type names must not have - or . to be compatible with display and shape api calls. + var shapeType = name.Replace("--", "__").Replace("-", "__").Replace('.', '_'); + + if (String.IsNullOrEmpty(displayType)) { + return shapeType; + } + var firstBreakingSeparator = shapeType.IndexOf("__", StringComparison.OrdinalIgnoreCase); + if (firstBreakingSeparator <= 0) { + return (shapeType + "_" + displayType); + } + + return (shapeType.Substring(0, firstBreakingSeparator) + "_" + displayType + shapeType.Substring(firstBreakingSeparator)); + } + + private static IHtmlString CoerceHtmlString(object invoke) { + return invoke as IHtmlString ?? (invoke != null ? new HtmlString(invoke.ToString()) : null); + } + + private void EnsureWorkContext(Action action) { + var workContext = _wca.GetContext(); + if (workContext != null) { + action(); + } + else { + using (_wca.CreateWorkContextScope()) { + action(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateProcessorImpl.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateProcessorImpl.cs new file mode 100644 index 000000000..11ad834e5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/TemplateProcessorImpl.cs @@ -0,0 +1,10 @@ +using System; +using Orchard.DisplayManagement.Implementation; + +namespace Orchard.Templates.Services { + public abstract class TemplateProcessorImpl : ITemplateProcessor { + public abstract string Type { get; } + public abstract string Process(string template, string name, DisplayContext context = null, dynamic model = null); + public virtual void Verify(string template) { } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.Templates/Styles/Web.config new file mode 100644 index 000000000..6e8ebec36 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Styles/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates-admin.css b/src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates-admin.css new file mode 100644 index 000000000..9d2255346 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates-admin.css @@ -0,0 +1,7 @@ +.navicon-templates { + background-image: url(menu.templates.png) !important; +} + +.navicon-templates:hover { + background-position: 0 -30px !important; +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates.png b/src/Orchard.Web/Modules/Orchard.Templates/Styles/menu.templates.png new file mode 100644 index 0000000000000000000000000000000000000000..84f0078ab77de6582830758910d0d993ee14d912 GIT binary patch literal 1254 zcmaJ>Z)n_P98R}syRzA;Y_>u%WFkBDlDxUgdC7X$rI)04%sb2Vw8um=P2Rf;ZIWw} zde^hVuIDxd!5cbJ8D*dj1SeSZgRHPQbUIgytFjNnA)?qar*#uYR}g%!y{mqx3%q&% zGidbDK2zEUEkmIhT$Bf7$PXOTw^4CpF&(HJrvz8E4_{PJjBJ*EhJ z#l#&95v!uo@m@S)*$`(*)~nLMk3YhZfCc^_co=7B;G<|C1$@HEE*7Oy=7Vzadj zpA_X97uto0oa^R!iYgQeWWh&Tc9sGh$5AvxF^m^kc%4zxRf=BI*-`f(LPxdrysKL# zUiGL9SR-zTKuTAyVC3WRHOHn?3ls_&RaEj6AZf}ls`py4cHAWVSH`;5PHHp{sU&o) z5nDz1$n2jgv`>;b03=_~=cgmIAOvZd3HWHXj*Xd)tC%XR^XiCq z6&sOniRB|UR9wqWS=La!0(x_nYdJY9k4O3f_|AUa)U1NzsitT3wj#9kXQ3wBmVvLt zm)F(f?|NEVYPK)R>QLYi=~hl%0|WUpJJ&Wp$e=n z*GeC4)|v;JsAFu@8mDKZF$`f4sKyP3-fwL+o7#k6hn1-g@T!%K5*yz4{9~w3ygF-SFCrQ)j|I zOnx}__2hKpiR2r{Jw3aw23}YgXnEiUanWqP#O?D5$6GwvZB4`NBid|badYLSc^fYL z5uJ%{$jv`^p!>Rd5emV8kT;^2Q@;&_S z#s#e7*v60M%X8_`%ST(bl;YBme*a literal 0 HcmV?d00001 diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Styles/template-editor.css b/src/Orchard.Web/Modules/Orchard.Templates/Styles/template-editor.css new file mode 100644 index 000000000..4261299fe --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Styles/template-editor.css @@ -0,0 +1,4 @@ +.CodeMirror { + border: 1px solid #bdbcbc; + height: 400px; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Orchard.Templates.Tests.csproj b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Orchard.Templates.Tests.csproj new file mode 100644 index 000000000..1f7eb0bd4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Orchard.Templates.Tests.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD} + Library + Properties + Orchard.Templates.Tests + Orchard.Templates.Tests + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\..\..\..\lib\autofac\Autofac.dll + + + ..\..\..\..\..\..\lib\moq\Moq.dll + + + ..\..\..\..\..\..\lib\nunit\nunit.framework.dll + + + + + + False + ..\..\..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + {ABC826D4-2FA1-4F2F-87DE-E6095F653810} + Orchard.Framework.Tests + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {10ab3ce2-a720-467f-9ec8-ebb4bac9a1c9} + Orchard.Templates + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1599fcdeb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Templating.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Orchard.Templating.Tests")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c7fb7f35-0c51-418a-86eb-71d7a601570a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/RazorParserTests.cs b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/RazorParserTests.cs new file mode 100644 index 000000000..e5f538c24 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/RazorParserTests.cs @@ -0,0 +1,24 @@ +using Autofac; +using NUnit.Framework; +using Orchard.Templates.Services; + +namespace Orchard.Templates.Tests { + [TestFixture] + public class RazorParserTests { + private IContainer _container; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + _container = builder.Build(); + } + + [Test] + public void ParseSomething() { + var razorParser = _container.Resolve(); + var result = razorParser.Process("@{ int i = 42;} The answer to everything is @i.", "Foo"); + Assert.That(result.Trim(), Is.EqualTo("The answer to everything is 42.")); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/TemplateServiceTests.cs b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/TemplateServiceTests.cs new file mode 100644 index 000000000..117729a07 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Tests/Orchard.Templates.Tests/TemplateServiceTests.cs @@ -0,0 +1,65 @@ +using System.Web; +using System.Web.Routing; +using Autofac; +using Moq; +using NUnit.Framework; +using Orchard.Caching; +using Orchard.DisplayManagement; +using Orchard.DisplayManagement.Descriptors; +using Orchard.DisplayManagement.Descriptors.ShapeAttributeStrategy; +using Orchard.DisplayManagement.Implementation; +using Orchard.Environment; +using Orchard.Environment.Extensions.Models; +using Orchard.Templates.Services; +using Orchard.Tests.DisplayManagement; +using Orchard.Tests.Stubs; +using Orchard.Tests.Utility; + +namespace Orchard.Templates.Tests { + [TestFixture] + public class TemplateServiceTests { + private IContainer _container; + + [SetUp] + public void Init() { + var workContext = new DefaultDisplayManagerTests.TestWorkContext { + CurrentTheme = new ExtensionDescriptor { Id = "Hello" } + }; + + var builder = new ContainerBuilder(); + builder.RegisterModule(new ShapeAttributeBindingModule()); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterInstance(new DefaultDisplayManagerTests.TestWorkContextAccessor(workContext)).As(); + builder.RegisterInstance(new SimpleShapes()); + builder.RegisterInstance(new RouteCollection()); + builder.RegisterType().As(); + builder.RegisterAutoMocking(MockBehavior.Loose); + + _container = builder.Build(); + _container.Resolve>() + .Setup(x => x.Resolve()) + .Returns(_container); + } + + public class SimpleShapes { + [Shape] + public IHtmlString Something() { + return new HtmlString("
"); + } + } + + [Test] + public void RenderSomething() { + var templateService = _container.Resolve(); + var result = templateService.ExecuteShape("Something"); + Assert.That(result, Is.EqualTo("
")); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Tokens/ShapeTokenProvider.cs b/src/Orchard.Web/Modules/Orchard.Templates/Tokens/ShapeTokenProvider.cs new file mode 100644 index 000000000..1146d9da2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Tokens/ShapeTokenProvider.cs @@ -0,0 +1,29 @@ +using System; +using Orchard.DisplayManagement; +using Orchard.Templates.Services; +using Orchard.Tokens; + +namespace Orchard.Templates.Tokens { + public class ShapeTokenProvider : Component, ITokenProvider { + private readonly ITemplateService _templateService; + + public ShapeTokenProvider(ITemplateService templateService) { + _templateService = templateService; + } + + public void Describe(DescribeContext context) { + context.For("Shape", T("Shape"), T("Shape tokens.")) + .Token("Execute:*", T("Execute:"), T("Executes the specified shape.")); + } + + public void Evaluate(EvaluateContext context) { + context.For("Shape", "") + .Token(t => t.StartsWith("Execute:", StringComparison.OrdinalIgnoreCase) ? t.Substring("Execute:".Length) : null, (shapeName, data) => TokenValue(context, shapeName, data)); + } + + private object TokenValue(EvaluateContext context, string shapeName, string data) { + var parameters = Arguments.From(context.Data.Values, context.Data.Keys); + return _templateService.ExecuteShape(shapeName, parameters); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/ViewModels/ShapePartViewModel.cs b/src/Orchard.Web/Modules/Orchard.Templates/ViewModels/ShapePartViewModel.cs new file mode 100644 index 000000000..5fe1ebf0e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/ViewModels/ShapePartViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Templates.ViewModels { + public class ShapePartViewModel { + [StringLength(100)] + public string Name { get; set; } + [UIHint("TemplateLanguagePicker")] + public string Language { get; set; } + public string Template { get; set; } + public IEnumerable AvailableLanguages { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/CreatableTypeList.cshtml b/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/CreatableTypeList.cshtml new file mode 100644 index 000000000..f6cbd2507 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/CreatableTypeList.cshtml @@ -0,0 +1,5 @@ +@{ Layout.Title = T("Create New Template").ToString(); } + +@foreach (var type in Model.ContentTypes) { +

@Html.ActionLink((string)type.DisplayName, "Create", new { area = "Contents", id = (string)type.Name, returnUrl = Request.QueryString["ReturnUrl"] })

+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/List.cshtml b/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/List.cshtml new file mode 100644 index 000000000..682f94bc2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/Admin/List.cshtml @@ -0,0 +1,48 @@ +@using Orchard.Core.Contents.ViewModels; +@{ + var typeDisplayName = Model.TypeDisplayName; + var pageTitle = T("Manage Templates"); + var createLinkText = T("Create New Template"); + if (!string.IsNullOrWhiteSpace(typeDisplayName)) { + pageTitle = T("Manage {0}s", typeDisplayName); + createLinkText = T("Create New {0}", typeDisplayName); + } + + Layout.Title = pageTitle; +} + +
+ @Html.ActionLink(createLinkText.Text, "Create", new { Area = "Orchard.Templates", Id = (string)Model.Options.SelectedFilter }, new { @class = "button primaryAction" }) +
+@using (Html.BeginFormAntiForgeryPost()) { +
+ + + +
+
+ + + + + +
+
+@Display(Model.ContentItems) +
+@Display(Model.Pager) +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/Parts.Shape.cshtml b/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/Parts.Shape.cshtml new file mode 100644 index 000000000..70d59aeb1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/Parts.Shape.cshtml @@ -0,0 +1,31 @@ +@model Orchard.Templates.ViewModels.ShapePartViewModel +@{ + Style.Include("~/modules/orchard.templates/scripts/codemirror/lib/codemirror.css"); + Style.Include("~/modules/orchard.templates/scripts/codemirror/theme/codemirror.css"); + Style.Include("template-editor.css"); + Script.Require("jQuery"); + Script.Include("codemirror/lib/codemirror.js"); + Script.Include("codemirror/mode/xml/xml.js"); + Script.Include("codemirror/mode/javascript/javascript.js"); + Script.Include("codemirror/mode/css/css.js"); + Script.Include("codemirror/mode/htmlmixed/htmlmixed.js"); + Script.Include("codemirror/mode/htmlembedded/htmlembedded.js"); + Script.Include("template-editor.js", "template-editor.min.js"); +} +
+
+ @Html.LabelFor(m => m.Name, T("Name")) + @Html.TextBoxFor(m => m.Name, new { @class = "text medium" }) + @T("The name of this template, which can be used to reference from code (as when using shape type names) and tokens.") +
+
+ @Html.LabelFor(m => m.Template, T("Template Body")) + @Html.TextAreaFor(m => m.Template, new { @class = "text large code-editor" }) + @T("The template.") +
+
+ @Html.LabelFor(m => m.Language, T("Language")) + @Html.EditorFor(m => m.Language, new { Model.AvailableLanguages }) + @T("The template language to use.") +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/TemplateLanguagePicker.cshtml b/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/TemplateLanguagePicker.cshtml new file mode 100644 index 000000000..ed46970ae --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/EditorTemplates/TemplateLanguagePicker.cshtml @@ -0,0 +1,6 @@ +@{ + var languages = (IList)ViewBag.AvailableLanguages; + var currentValue = ViewData.TemplateInfo.FormattedModelValue as string; + var options = languages.Select(x => new SelectListItem() {Text = x, Value = x, Selected = x == currentValue}); +} +@Html.DropDownList("", options) \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/SampleShape.cshtml b/src/Orchard.Web/Modules/Orchard.Templates/Views/SampleShape.cshtml new file mode 100644 index 000000000..088a82f1b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/SampleShape.cshtml @@ -0,0 +1 @@ +This is a sample shape for testing purposes. Don't get too attached with it, because it will eventually be removed. \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Views/Web.config b/src/Orchard.Web/Modules/Orchard.Templates/Views/Web.config new file mode 100644 index 000000000..f9e129ffc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Web.config b/src/Orchard.Web/Modules/Orchard.Templates/Web.config new file mode 100644 index 000000000..47b141313 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Templates/Web.config @@ -0,0 +1,46 @@ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Orchard.Web.csproj b/src/Orchard.Web/Orchard.Web.csproj index 0ff72fb9e..e2183ce23 100644 --- a/src/Orchard.Web/Orchard.Web.csproj +++ b/src/Orchard.Web/Orchard.Web.csproj @@ -163,7 +163,6 @@ Web.config - Designer diff --git a/src/Orchard.sln b/src/Orchard.sln index 9bb0bdaff..68544bd21 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -231,6 +231,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure", "Orchard.We EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.SecureSocketsLayer", "Orchard.Web\Modules\Orchard.SecureSocketsLayer\Orchard.SecureSocketsLayer.csproj", "{36B82383-D69E-4897-A24A-648BABDF80EC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Templates", "Orchard.Web\Modules\Orchard.Templates\Orchard.Templates.csproj", "{10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Templates.Tests", "Orchard.Web\Modules\Orchard.Templates\Tests\Orchard.Templates.Tests\Orchard.Templates.Tests.csproj", "{8961613C-0ADD-4DF3-945B-CAE47987CAFD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Messaging.Tests", "Orchard.Web\Modules\Orchard.Messaging\Tests\Orchard.Messaging.Tests.csproj", "{2B4E039D-EDEB-43DD-8F5A-C640035906A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeCoverage|Any CPU = CodeCoverage|Any CPU @@ -959,6 +965,36 @@ Global {36B82383-D69E-4897-A24A-648BABDF80EC}.FxCop|Any CPU.Build.0 = Release|Any CPU {36B82383-D69E-4897-A24A-648BABDF80EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {36B82383-D69E-4897-A24A-648BABDF80EC}.Release|Any CPU.Build.0 = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Coverage|Any CPU.Build.0 = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.FxCop|Any CPU.Build.0 = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9}.Release|Any CPU.Build.0 = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Coverage|Any CPU.Build.0 = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.FxCop|Any CPU.Build.0 = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Release|Any CPU.Build.0 = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Coverage|Any CPU.Build.0 = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.FxCop|Any CPU.Build.0 = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1019,12 +1055,15 @@ Global {8A9FDB57-342D-49C2-BAFC-D885AAE5CC7C} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {CBC7993C-57D8-4A6C-992C-19E849DFE71D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {36B82383-D69E-4897-A24A-648BABDF80EC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {2FC1D9C8-446D-4414-B252-5E9FBE61EB63} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {E07AFA7E-7B36-44C3-A537-AFCCAA93EA7A} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {2969635F-D9C3-4D01-890D-437B46659690} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} + {8961613C-0ADD-4DF3-945B-CAE47987CAFD} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} + {2B4E039D-EDEB-43DD-8F5A-C640035906A9} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {5E5E7A21-C7B2-44D8-8593-2F9541AE041D} = {383DBA32-4A3E-48D1-AAC3-75377A694452} {33B1BC8D-E292-4972-A363-22056B207156} = {383DBA32-4A3E-48D1-AAC3-75377A694452} {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE} = {383DBA32-4A3E-48D1-AAC3-75377A694452} diff --git a/src/Orchard/Compilation/CompilersModule.cs b/src/Orchard/Compilation/CompilersModule.cs new file mode 100644 index 000000000..9f6daadd4 --- /dev/null +++ b/src/Orchard/Compilation/CompilersModule.cs @@ -0,0 +1,8 @@ +using Autofac; + +namespace Orchard.Compilation { + public class CompilersModule : Module { + protected override void Load(ContainerBuilder builder) { + } + } +} diff --git a/src/Orchard/Compilation/ICompiler.cs b/src/Orchard/Compilation/ICompiler.cs new file mode 100644 index 000000000..d9161a5cd --- /dev/null +++ b/src/Orchard/Compilation/ICompiler.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Orchard.Compilation { + public interface ICompiler{ + object Compile(string code, IDictionary parameters); + T Compile(string code, IDictionary parameters); + } +} \ No newline at end of file diff --git a/src/Orchard/Compilation/Razor/IRazorCompiler.cs b/src/Orchard/Compilation/Razor/IRazorCompiler.cs new file mode 100644 index 000000000..df8fa4cac --- /dev/null +++ b/src/Orchard/Compilation/Razor/IRazorCompiler.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace Orchard.Compilation.Razor { + public interface IRazorCompiler : ICompiler { + IRazorTemplateBase CompileRazor(string code, string name, IDictionary parameters); + IRazorTemplateBase CompileRazor(string code, string name, IDictionary parameters); + } +} \ No newline at end of file diff --git a/src/Orchard/Compilation/Razor/IRazorTemplateCache.cs b/src/Orchard/Compilation/Razor/IRazorTemplateCache.cs new file mode 100644 index 000000000..17b9cdd88 --- /dev/null +++ b/src/Orchard/Compilation/Razor/IRazorTemplateCache.cs @@ -0,0 +1,6 @@ +namespace Orchard.Compilation.Razor { + public interface IRazorTemplateCache { + string Get(string name); + void Set(string name, string template); + } +} \ No newline at end of file diff --git a/src/Orchard/Compilation/Razor/RazorCompiler.cs b/src/Orchard/Compilation/Razor/RazorCompiler.cs new file mode 100644 index 000000000..9ce2a10b9 --- /dev/null +++ b/src/Orchard/Compilation/Razor/RazorCompiler.cs @@ -0,0 +1,152 @@ +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Web.Razor; +using System.Web.Razor.Generator; +using Microsoft.CSharp; +using Orchard.Caching; +using Orchard.Logging; +using Orchard.Utility.Extensions; + +namespace Orchard.Compilation.Razor { + public class RazorCompiler : IRazorCompiler { + private readonly ICacheManager _cache; + private readonly IWorkContextAccessor _wca; + const string DynamicallyGeneratedClassName = "RazorTemplate"; + const string NamespaceForDynamicClasses = "Orchard.Compilation.Razor"; + private const string ForceRecompile = "Razor.ForceRecompile"; + + public RazorCompiler(ICacheManager cache, IWorkContextAccessor wca) { + _cache = cache; + _wca = wca; + Logger = NullLogger.Instance; + } + + ILogger Logger { get; set; } + + public IRazorTemplateBase CompileRazor(string code, string name, IDictionary parameters) { + return (RazorTemplateBase)Compile(code, name, typeof(TModel), parameters); + } + + public IRazorTemplateBase CompileRazor(string code, string name, IDictionary parameters) { + return (IRazorTemplateBase)Compile(code, name, null, parameters); + } + + public object Compile(string code, IDictionary parameters) { + return CompileRazor>(code, null, parameters); + } + + public T Compile(string code, IDictionary parameters) { + return (T) Compile(code, null, null, parameters); + } + + private object Compile(string code, string name, Type modelType, IDictionary parameters) { + ISignals signals = _wca.GetContext().TryResolve(out signals) ? signals : null; + + var cacheKey = name ?? GetHash(code); + var generatedClassName = name != null ? name.Strip(c => !c.IsLetter() && !Char.IsDigit(c)) : DynamicallyGeneratedClassName; + + var assembly = _cache.Get(cacheKey, ctx => { + if (signals != null) { + ctx.Monitor(signals.When(ForceRecompile)); + } + + var modelTypeName = "dynamic"; + var reader = new StringReader(code); + var builder = new StringBuilder(); + + // A hack to remove any @model directive as it's MVC-specific and compiler does not recognize it. + // We should use this information to compile a strongly-typed template in the future + string line; + while ((line = reader.ReadLine()) != null) { + var trimmedLine = line.TrimStart(' ', '\t', '\n', '\r'); + if (trimmedLine.StartsWith("@model ")) { + modelTypeName = trimmedLine.Substring("@model ".Length).Trim(); + continue; + } + + builder.AppendLine(line); + } + + var language = new CSharpRazorCodeLanguage(); + var host = new RazorEngineHost(language) { + DefaultBaseClass = "RazorTemplateBase<" + modelTypeName + ">", + DefaultClassName = generatedClassName, + DefaultNamespace = NamespaceForDynamicClasses + }; + + var namespaces = new List { + "System", + "System.IO", + "System.Linq", + "System.Collections", + "System.Collections.Generic", + "System.Dynamic", + "System.Text", + "System.Web", + "System.Web.Mvc", + "System.Web.Mvc.Html", + "System.Web.Mvc.Ajax", + "System.Web.UI", + "System.Web.Routing", + "Orchard.Compilation.Razor", + "Orchard.ContentManagement", + "Orchard.DisplayManagement", + "Orchard.DisplayManagement.Shapes", + "Orchard.Security.Permissions", + "Orchard.UI.Resources", + "Orchard.Security", + "Orchard.Mvc.Spooling", + "Orchard.Mvc.Html" + }; + + foreach (var n in namespaces) { + host.NamespaceImports.Add(n); + } + + var engine = new RazorTemplateEngine(host); + var razorTemplate = engine.GenerateCode(new StringReader(builder.ToString())); + var compiledAssembly = CreateCompiledAssemblyFor(razorTemplate.GeneratedCode, name); + return compiledAssembly; + }); + + return assembly.CreateInstance(NamespaceForDynamicClasses + "." + generatedClassName); + } + + public static string GetHash(string value) + { + var data = Encoding.ASCII.GetBytes(value); + var hashData = new SHA1Managed().ComputeHash(data); + + return hashData.Aggregate(string.Empty, (current, b) => current + b.ToString("X2")); + } + + private static Assembly CreateCompiledAssemblyFor(CodeCompileUnit unitToCompile, string templateName) + { + var compilerParameters = new CompilerParameters(); + compilerParameters.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain + .GetAssemblies() + .Where(a => !a.IsDynamic) + .Select(a => a.Location) + .ToArray()); + + compilerParameters.GenerateInMemory = true; + + var compilerResults = new CSharpCodeProvider().CompileAssemblyFromDom(compilerParameters, unitToCompile); + if (compilerResults.Errors.HasErrors) { + var errors = compilerResults.Errors.Cast().Aggregate(string.Empty, (s, error) => s + "\r\nTemplate '" + templateName + "': " + error.ToString()); + throw new Exception(string.Format("Razor template compilation errors:\r\n{0}", errors)); + } + else { + var compiledAssembly = compilerResults.CompiledAssembly; + return compiledAssembly; + } + } + } +} \ No newline at end of file diff --git a/src/Orchard/Compilation/Razor/RazorModule.cs b/src/Orchard/Compilation/Razor/RazorModule.cs new file mode 100644 index 000000000..7c6b79ace --- /dev/null +++ b/src/Orchard/Compilation/Razor/RazorModule.cs @@ -0,0 +1,10 @@ +using Autofac; +using Orchard.ContentManagement; + +namespace Orchard.Compilation.Razor { + public class RazorModule : Module { + protected override void Load(ContainerBuilder builder) { + + } + } +} diff --git a/src/Orchard/Compilation/Razor/RazorTemplateBase.cs b/src/Orchard/Compilation/Razor/RazorTemplateBase.cs new file mode 100644 index 000000000..46f509137 --- /dev/null +++ b/src/Orchard/Compilation/Razor/RazorTemplateBase.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Web.Mvc; +using System.Web.UI; +using System.Web.WebPages; +using System.Web.WebPages.Instrumentation; + +namespace Orchard.Compilation.Razor { + public interface IRazorTemplateBase + { + dynamic Model { get; } + WebPageContext WebPageContext { get; set; } + ViewContext ViewContext { get; set; } + ViewDataDictionary ViewData { get; set; } + string VirtualPath { get; set; } + void Render(TextWriter writer); + void InitHelpers(); + + } + + public interface IRazorTemplateBase : IRazorTemplateBase + { + TModel Model { get; } + ViewDataDictionary ViewData { get; set; } + } + + public abstract class RazorTemplateBase : Mvc.ViewEngines.Razor.WebViewPage, IRazorTemplateBase { + public WebPageContext WebPageContext { get; set; } + public void Render(TextWriter writer) { + PushContext(WebPageContext, writer); + OutputStack.Push(writer); + Execute(); + OutputStack.Pop(); + PopContext(); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Compilation/Razor/RazorTemplateCache.cs b/src/Orchard/Compilation/Razor/RazorTemplateCache.cs new file mode 100644 index 000000000..be381e0c6 --- /dev/null +++ b/src/Orchard/Compilation/Razor/RazorTemplateCache.cs @@ -0,0 +1,19 @@ +using System.Collections.Concurrent; + +namespace Orchard.Compilation.Razor { + public class RazorTemplateCache : IRazorTemplateCache { + private readonly ConcurrentDictionary _cache; + + public RazorTemplateCache() { + _cache = new ConcurrentDictionary(); + } + public string Get(string name) { + string template; + return _cache.TryGetValue(name, out template) ? template : null; + } + + public void Set(string name, string template) { + _cache.AddOrUpdate(name, template, (s, s1) => template); + } + } +} \ No newline at end of file diff --git a/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs b/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs index a426f1938..fa4f43984 100644 --- a/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs +++ b/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,11 +7,13 @@ using System.Web.Mvc; using System.Web.Mvc.Html; using Orchard.Caching; using Orchard.DisplayManagement.Implementation; +using Orchard.Environment; using Orchard.Environment.Descriptor.Models; using Orchard.Environment.Extensions; using Orchard.Environment.Extensions.Models; using Orchard.FileSystems.VirtualPath; using Orchard.Logging; +using Orchard.Mvc.ViewEngines.ThemeAwareness; using Orchard.Utility.Extensions; namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { @@ -25,7 +26,8 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { private readonly IEnumerable _harvesters; private readonly IEnumerable _shapeTemplateViewEngines; private readonly IParallelCacheContext _parallelCacheContext; - + private readonly Work _viewEngine; + private readonly IWorkContextAccessor _workContextAccessor; public ShapeTemplateBindingStrategy( IEnumerable harvesters, @@ -35,7 +37,9 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { IVirtualPathMonitor virtualPathMonitor, IVirtualPathProvider virtualPathProvider, IEnumerable shapeTemplateViewEngines, - IParallelCacheContext parallelCacheContext) { + IParallelCacheContext parallelCacheContext, + Work viewEngine, + IWorkContextAccessor workContextAccessor) { _harvesters = harvesters; _shellDescriptor = shellDescriptor; @@ -45,6 +49,8 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { _virtualPathProvider = virtualPathProvider; _shapeTemplateViewEngines = shapeTemplateViewEngines; _parallelCacheContext = parallelCacheContext; + _viewEngine = viewEngine; + _workContextAccessor = workContextAccessor; Logger = NullLogger.Instance; } @@ -137,13 +143,46 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { private IHtmlString Render(ShapeDescriptor shapeDescriptor, DisplayContext displayContext, HarvestShapeInfo harvestShapeInfo, HarvestShapeHit harvestShapeHit) { Logger.Information("Rendering template file '{0}'", harvestShapeInfo.TemplateVirtualPath); + IHtmlString result; - var htmlHelper = new HtmlHelper(displayContext.ViewContext, displayContext.ViewDataContainer); - var result = htmlHelper.Partial(harvestShapeInfo.TemplateVirtualPath, displayContext.Value); + if (displayContext.ViewContext.View != null) { + var htmlHelper = new HtmlHelper(displayContext.ViewContext, displayContext.ViewDataContainer); + result = htmlHelper.Partial(harvestShapeInfo.TemplateVirtualPath, displayContext.Value); + } + else { + // If the View is null, it means that the shape is being executed from a non-view origin / where no ViewContext was established by the view engine, but manually. + // Manually creating a ViewContext works when working with Shape methods, but not when the shape is implemented as a Razor view template. + // Horrible, but it will have to do for now. + result = RenderRazorViewToString(harvestShapeInfo.TemplateVirtualPath, displayContext.Value); + } Logger.Information("Done rendering template file '{0}'", harvestShapeInfo.TemplateVirtualPath); return result; } + private IHtmlString RenderRazorViewToString(string path, object model) { + using (var sw = new StringWriter()) { + var controllerContext = CreateControllerContext(); + var viewResult = _viewEngine.Value.FindPartialView(controllerContext, path, false); + var viewContext = new ViewContext(controllerContext, viewResult.View, new ViewDataDictionary(model), new TempDataDictionary(), sw); + viewResult.View.Render(viewContext, sw); + viewResult.ViewEngine.ReleaseView(controllerContext, viewResult.View); + return new HtmlString(sw.GetStringBuilder().ToString()); + } + } + + private ControllerContext CreateControllerContext() { + var controller = new StubController(); + var httpContext = _workContextAccessor.GetContext().HttpContext; + var routeData = httpContext.Request.RequestContext.RouteData; + + if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller")) + routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", "")); + + controller.ControllerContext = new ControllerContext(httpContext, routeData, controller); + return controller.ControllerContext; + } + + private class StubController : Controller { } } } diff --git a/src/Orchard/Messaging/Services/IMessagingChannel.cs b/src/Orchard/Messaging/Services/IMessagingChannel.cs index 2b31ec25b..0834679a3 100644 --- a/src/Orchard/Messaging/Services/IMessagingChannel.cs +++ b/src/Orchard/Messaging/Services/IMessagingChannel.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Orchard.Messaging.Models; namespace Orchard.Messaging.Services { + [Obsolete("Use the new IMessageChannel interface instead.")] public interface IMessagingChannel : IDependency { /// /// Actually sends the message though this channel diff --git a/src/Orchard/Mvc/IHttpContextAccessor.cs b/src/Orchard/Mvc/IHttpContextAccessor.cs index ee79e4527..f7df5cb97 100644 --- a/src/Orchard/Mvc/IHttpContextAccessor.cs +++ b/src/Orchard/Mvc/IHttpContextAccessor.cs @@ -4,14 +4,19 @@ using System.Web; namespace Orchard.Mvc { public interface IHttpContextAccessor { HttpContextBase Current(); + void Set(HttpContextBase stub); } public class HttpContextAccessor : IHttpContextAccessor { + private HttpContextBase _stub; + public HttpContextBase Current() { var httpContext = GetStaticProperty(); - if (httpContext == null) - return null; - return new HttpContextWrapper(httpContext); + return httpContext != null ? new HttpContextWrapper(httpContext) : _stub; + } + + public void Set(HttpContextBase stub) { + _stub = stub; } private HttpContext GetStaticProperty() { diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 1756e6c8b..35dcdda28 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -149,6 +149,14 @@ + + + + + + + +