parent
ea45a2194d
commit
88a5e787ce
@ -0,0 +1,19 @@ |
||||
// Wrapper for inline text editor |
||||
.op-ckeditor-element |
||||
min-height: 50px |
||||
border: 1px solid #bfbfbf !important |
||||
|
||||
&.ck-editor__editable_inline |
||||
padding-left: 2px !important |
||||
|
||||
// Wrapper for full text element |
||||
.op-ckeditor--wrapper |
||||
|
||||
// Borders for the main editor |
||||
.ck-editor__main |
||||
border: 1px solid #bfbfbf |
||||
margin-bottom: 2rem |
||||
|
||||
// Min height for the editable section |
||||
.ck-editor__editable |
||||
min-height: 20vh |
@ -0,0 +1,117 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import IAugmentedJQuery = angular.IAugmentedJQuery; |
||||
import {IDialogService} from 'ng-dialog'; |
||||
import {IDialogScope} from 'ng-dialog'; |
||||
import {opUiComponentsModule} from '../../angular-modules'; |
||||
|
||||
export interface ICkeditorInstance { |
||||
getData():string; |
||||
setData(content:string):void; |
||||
} |
||||
|
||||
export interface ICkeditorStatic { |
||||
create(el:HTMLElement):Promise<ICkeditorInstance>; |
||||
} |
||||
|
||||
declare global { |
||||
interface Window { |
||||
BalloonEditor:ICkeditorStatic; |
||||
ClassicEditor:ICkeditorStatic; |
||||
} |
||||
} |
||||
|
||||
const ckEditorWrapperClass = 'op-ckeditor--wrapper'; |
||||
const ckEditorReplacementClass = '__op_ckeditor_replacement_container'; |
||||
|
||||
export class OpCkeditorFormComponent { |
||||
public textareaSelector:string; |
||||
|
||||
// Which template to include
|
||||
public ckeditor:any; |
||||
public formElement:JQuery; |
||||
public wrappedTextArea:JQuery; |
||||
|
||||
// Remember if the user changed
|
||||
public changed:boolean = false; |
||||
public inFlight:boolean = false; |
||||
|
||||
public text:any; |
||||
|
||||
|
||||
constructor(protected $element:ng.IAugmentedJQuery, |
||||
protected $timeout:ng.ITimeoutService, |
||||
protected ConfigurationService:any, |
||||
protected I18n:op.I18n) { |
||||
|
||||
} |
||||
|
||||
public $onInit() { |
||||
this.formElement = this.$element.closest('form'); |
||||
this.wrappedTextArea = this.formElement.find(this.textareaSelector); |
||||
const wrapper = this.$element.find(`.${ckEditorReplacementClass}`); |
||||
window.ClassicEditor |
||||
.create(wrapper[0]) |
||||
.then(this.setup.bind(this)) |
||||
.catch((error:any) => { |
||||
console.error(error); |
||||
}); |
||||
} |
||||
|
||||
public $onDestroy() { |
||||
this.formElement.off('submit.ckeditor'); |
||||
} |
||||
|
||||
public setup(editor:ICkeditorInstance) { |
||||
this.ckeditor = editor; |
||||
const rawValue = this.wrappedTextArea.val(); |
||||
|
||||
if (rawValue) { |
||||
editor.setData(rawValue); |
||||
} |
||||
|
||||
// Listen for form submission to set textarea content
|
||||
this.formElement.on('submit.ckeditor', () => { |
||||
const value = this.ckeditor.getData(); |
||||
this.wrappedTextArea.val(value); |
||||
|
||||
// Continue with submission
|
||||
return true; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
opUiComponentsModule.component('opCkeditorForm', { |
||||
template: `<div class="${ckEditorWrapperClass}"><div class="${ckEditorReplacementClass}"></div>`, |
||||
controller: OpCkeditorFormComponent, |
||||
controllerAs: '$ctrl', |
||||
bindings: { |
||||
textareaSelector: '@' |
||||
} |
||||
}); |
@ -0,0 +1,22 @@ |
||||
<div class="textarea-wrapper op-ckeditor-wrapper" ng-class="{'-preview': vm.field.isPreview}"> |
||||
<div class="op-ckeditor-element" ng-class="{ '-active': $ctrl.isChanged }"> |
||||
</div> |
||||
<div class="inplace-edit--dashboard"> |
||||
<div class="inplace-edit--controls" ng-show="$ctrl.isChanged || $ctrl.inFlight"> |
||||
<accessible-by-keyboard execute="$ctrl.submit()" |
||||
link-title="{{ vm.saveTitle }}" |
||||
is-disabled="$ctrl.inFlight" |
||||
ng-disabled="$ctrl.inFlight" |
||||
class="inplace-edit--control inplace-edit--control--save"> |
||||
<op-icon icon-classes="icon-checkmark" icon-title="{{ ::vm.saveTitle }}"></op-icon> |
||||
</accessible-by-keyboard> |
||||
<accessible-by-keyboard execute="$ctrl.reset()" |
||||
link-title="{{ vm.cancelTitle }}" |
||||
ng-disabled="$ctrl.inFlight" |
||||
is-disabled="$ctrl.inFlight" |
||||
class="inplace-edit--control inplace-edit--control--cancel"> |
||||
<op-icon icon-classes="icon-close" icon-title="{{ vm.cancelTitle }}"></op-icon> |
||||
</accessible-by-keyboard> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,145 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import IAugmentedJQuery = angular.IAugmentedJQuery; |
||||
import {IDialogService} from 'ng-dialog'; |
||||
import {IDialogScope} from 'ng-dialog'; |
||||
import {opUiComponentsModule} from '../../../angular-modules'; |
||||
import {HelpTextResourceInterface} from '../../api/api-v3/hal-resources/help-text-resource.service'; |
||||
import {HelpTextDmService} from '../../api/api-v3/hal-resource-dms/help-text-dm.service'; |
||||
import {opWorkPackagesModule} from './../../../angular-modules'; |
||||
import {WorkPackageChangeset} from './../../wp-edit-form/work-package-changeset'; |
||||
import {WorkPackageResourceInterface} from './../../api/api-v3/hal-resources/work-package-resource.service'; |
||||
import { WorkPackageEditFieldGroupController } from 'app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; |
||||
|
||||
export class WorkPackageEditorFieldController { |
||||
public wpEditFieldGroup:WorkPackageEditFieldGroupController; |
||||
public workPackage:WorkPackageResourceInterface; |
||||
public attribute:string; |
||||
public wrapperClasses:string; |
||||
|
||||
// Which template to include
|
||||
public format:string; |
||||
public ckeditor:any; |
||||
|
||||
// Remember if the user changed
|
||||
public changed:boolean = false; |
||||
public inFlight:boolean = false; |
||||
|
||||
public text:any; |
||||
|
||||
|
||||
constructor(protected $element:ng.IAugmentedJQuery, |
||||
protected $timeout:ng.ITimeoutService, |
||||
protected ConfigurationService:any, |
||||
protected I18n:op.I18n) { |
||||
|
||||
this.text = { |
||||
saveTitle: 'Save', |
||||
cancelTitle: 'Cancel' |
||||
}; |
||||
|
||||
// if(ConfigurationService.text_formatting == 'markdown') {
|
||||
this.format = 'markdown'; |
||||
// } else {
|
||||
// }
|
||||
|
||||
} |
||||
|
||||
public $onInit() { |
||||
const element = this.$element.find('.op-ckeditor-element'); |
||||
(window as any).BalloonEditor |
||||
.create(element[0]) |
||||
.then((editor:any) => { |
||||
this.ckeditor = editor; |
||||
if (this.rawValue) { |
||||
this.reset(); |
||||
} |
||||
}) |
||||
.catch((error:any) => { |
||||
console.error(error); |
||||
}); |
||||
} |
||||
|
||||
public submit() { |
||||
this.inFlight = true; |
||||
this.value = this.ckeditor.getData(); |
||||
this.wpEditFieldGroup.saveWorkPackage().then(() => { |
||||
this.reset(); |
||||
}) |
||||
.catch(() => { |
||||
this.reset(); |
||||
}); |
||||
} |
||||
|
||||
public reset() { |
||||
this.ckeditor.setData(this.rawValue); |
||||
this.$timeout(() => { |
||||
this.changed = false; |
||||
this.inFlight = false; |
||||
}); |
||||
} |
||||
|
||||
public get isInitialized() { |
||||
return !!this.ckeditor; |
||||
} |
||||
|
||||
public get value() { |
||||
return this.changeset.value(this.attribute); |
||||
} |
||||
|
||||
public get rawValue() { |
||||
if (this.value && this.value.raw) { |
||||
return this.value.raw; |
||||
} else { |
||||
return ''; |
||||
} |
||||
} |
||||
|
||||
public set value(value:any) { |
||||
this.changeset.setValue(this.attribute, { raw: value }); |
||||
} |
||||
|
||||
public get changeset():WorkPackageChangeset { |
||||
return this.wpEditFieldGroup.form.changeset; |
||||
} |
||||
} |
||||
|
||||
opWorkPackagesModule.component('wpEditorField', { |
||||
templateUrl: '/components/work-packages/wp-editor-field/wp-editor-field.component.html', |
||||
controller: WorkPackageEditorFieldController, |
||||
require: { |
||||
wpEditFieldGroup: '^wpEditFieldGroup' |
||||
}, |
||||
controllerAs: '$ctrl', |
||||
bindings: { |
||||
workPackage: '<', |
||||
attribute: '<', |
||||
wrapperClasses: '@' |
||||
} |
||||
}); |
@ -0,0 +1,12 @@ |
||||
<div class="textarea-wrapper"> |
||||
<div class="op-ckeditor-wrapper op-ckeditor-element"> |
||||
</div> |
||||
<wp-edit-field-controls ng-show="!vm.inEditMode" |
||||
field-controller="vm" |
||||
on-save="vm.handleUserSubmit()" |
||||
on-cancel="vm.handleUserCancel()" |
||||
save-title="{{ vm.field.text.save }}" |
||||
cancel-title="{{ vm.field.text.cancel }}"> |
||||
</wp-edit-field-controls> |
||||
</div> |
||||
</div> |
@ -0,0 +1,80 @@ |
||||
const ClassicEditor = (require('./ckeditor5/packages/ckeditor5-editor-classic/src/classiceditor') as any).default; |
||||
const BalloonEditor = (require('./ckeditor5/packages/ckeditor5-editor-balloon/src/ballooneditor') as any).default; |
||||
|
||||
const EssentialsPlugin = (require('./ckeditor5/packages/ckeditor5-essentials/src/essentials') as any).default; |
||||
const AutoformatPlugin = (require('./ckeditor5/packages/ckeditor5-autoformat/src/autoformat') as any).default; |
||||
const BoldPlugin = (require('./ckeditor5/packages/ckeditor5-basic-styles/src/bold') as any).default; |
||||
const ItalicPlugin = (require('./ckeditor5/packages/ckeditor5-basic-styles/src/italic') as any).default; |
||||
const BlockquotePlugin = (require('./ckeditor5/packages/ckeditor5-block-quote/src/blockquote') as any).default; |
||||
const HeadingPlugin = (require('./ckeditor5/packages/ckeditor5-heading/src/heading') as any).default; |
||||
const ImagePlugin = (require('./ckeditor5/packages/ckeditor5-image/src/image') as any).default; |
||||
const ImagecaptionPlugin = (require('./ckeditor5/packages/ckeditor5-image/src/imagecaption') as any).default; |
||||
const ImagestylePlugin = (require('./ckeditor5/packages/ckeditor5-image/src/imagestyle') as any).default; |
||||
const ImagetoolbarPlugin = (require('./ckeditor5/packages/ckeditor5-image/src/imagetoolbar') as any).default; |
||||
const LinkPlugin = (require('./ckeditor5/packages/ckeditor5-link/src/link') as any).default; |
||||
const ListPlugin = (require('./ckeditor5/packages/ckeditor5-list/src/list') as any).default; |
||||
const ParagraphPlugin = (require('./ckeditor5/packages/ckeditor5-paragraph/src/paragraph') as any).default; |
||||
// const GFMDataProcessor = (require('./ckeditor5/packages/ckeditor5-markdown-gfm/src/gfmdataprocessor') as any).default;
|
||||
// import OPCommonMarkProcessor from './plugins/op-commonmark/op-commonmark';
|
||||
const CommonMarkDataProcessor = (require('./plugins/ckeditor5-markdown-gfm/src/commonmarkdataprocessor') as any).default; |
||||
|
||||
// import OpTableWidget from './plugins/op-table/src/op-table';
|
||||
import OPImageUploadPlugin from './plugins/op-image-upload/op-image-upload'; |
||||
|
||||
function Markdown( editor:any ) { |
||||
editor.data.processor = new CommonMarkDataProcessor(); |
||||
} |
||||
|
||||
declare global { |
||||
var angular: any; |
||||
} |
||||
|
||||
export class OPClassicEditor extends ClassicEditor {} |
||||
export class OPBalloonEditor extends BalloonEditor {} |
||||
|
||||
(window as any).BalloonEditor = OPBalloonEditor; |
||||
(window as any).ClassicEditor = OPClassicEditor; |
||||
|
||||
const config = { |
||||
plugins: [ |
||||
// Markdown,
|
||||
EssentialsPlugin, |
||||
AutoformatPlugin, |
||||
BoldPlugin, |
||||
ItalicPlugin, |
||||
BlockquotePlugin, |
||||
HeadingPlugin, |
||||
ImagePlugin, |
||||
ImagecaptionPlugin, |
||||
ImagestylePlugin, |
||||
ImagetoolbarPlugin, |
||||
LinkPlugin, |
||||
ListPlugin, |
||||
ParagraphPlugin, |
||||
// OPImageUploadPlugin
|
||||
], |
||||
config: { |
||||
toolbar: [ |
||||
'headings', |
||||
'bold', |
||||
'italic', |
||||
'link', |
||||
'bulletedList', |
||||
'numberedList', |
||||
'blockQuote', |
||||
'undo', |
||||
'redo' |
||||
], |
||||
image: { |
||||
toolbar: [ |
||||
'imageStyleFull', |
||||
'imageStyleSide', |
||||
'|', |
||||
'imageTextAlternative' |
||||
] |
||||
} |
||||
} |
||||
}; |
||||
|
||||
(OPClassicEditor as any).build = config; |
||||
(OPBalloonEditor as any).build = config; |
@ -0,0 +1,40 @@ |
||||
const Plugin:any = (require('@ckeditor/ckeditor5-core/src/plugin') as any).default; |
||||
const Image:any = (require('@ckeditor/ckeditor5-image/src/image') as any).default; |
||||
const FileRepository:any = (require('@ckeditor/ckeditor5-upload/src/filerepository') as any).default; |
||||
const ImageUpload:any = (require('@ckeditor/ckeditor5-upload/src/imageupload') as any).default; |
||||
const ImageUploadEngine:any = (require('@ckeditor/ckeditor5-upload/src/imageuploadengine') as any).default; |
||||
|
||||
import { OpenProjectUploadAdapter } from './op-upload-adadpter'; |
||||
|
||||
interface CkEditorInstance { |
||||
plugins:any; |
||||
} |
||||
|
||||
interface IFileLoader { |
||||
file:File; |
||||
uploadTotal?:number; |
||||
uploaded?:number; |
||||
} |
||||
|
||||
export default class OPImageUploadPlugin extends Plugin { |
||||
public editor:CkEditorInstance; |
||||
|
||||
static get requires() { |
||||
return [ |
||||
Image, |
||||
ImageUpload, |
||||
ImageUploadEngine, |
||||
FileRepository |
||||
]; |
||||
} |
||||
|
||||
init() { |
||||
this.editor.plugins.get( FileRepository ).createAdapter = (loader:IFileLoader) => { |
||||
return new OpenProjectUploadAdapter(loader as any, this.editor); |
||||
}; |
||||
} |
||||
|
||||
static get pluginName() { |
||||
return 'OpenProject Image Upload'; |
||||
} |
||||
} |
@ -1,41 +0,0 @@ |
||||
Changelog |
||||
========= |
||||
|
||||
## [1.0.0-alpha.1](https://github.com/ckeditor/ckeditor5-block-quote/compare/v0.2.0...v1.0.0-alpha.1) (2017-10-03) |
||||
|
||||
### Other changes |
||||
|
||||
* Improved default blockquote styling so it does not overlap with floated images. Closes [#12](https://github.com/ckeditor/ckeditor5-block-quote/issues/12). ([fb09418](https://github.com/ckeditor/ckeditor5-block-quote/commit/fb09418)) |
||||
|
||||
|
||||
## [0.2.0](https://github.com/ckeditor/ckeditor5-block-quote/compare/v0.1.1...v0.2.0) (2017-09-03) |
||||
|
||||
### Features |
||||
|
||||
* <kbd>Enter</kbd> in the block quote will scroll the viewport to the selection. See ckeditor/ckeditor5-engine#660. ([09dc740](https://github.com/ckeditor/ckeditor5-block-quote/commit/09dc740)) |
||||
|
||||
### Other changes |
||||
|
||||
* Aligned the implementation to the new Command API (see https://github.com/ckeditor/ckeditor5-core/issues/88). ([627510a](https://github.com/ckeditor/ckeditor5-block-quote/commit/627510a)) |
||||
|
||||
### BREAKING CHANGES |
||||
|
||||
* The command API has been changed. |
||||
|
||||
|
||||
## [0.1.1](https://github.com/ckeditor/ckeditor5-block-quote/compare/v0.1.0...v0.1.1) (2017-05-07) |
||||
|
||||
### Bug fixes |
||||
|
||||
* Block quote should not be applied to image's caption. Closes: [#10](https://github.com/ckeditor/ckeditor5-block-quote/issues/10). ([06de874](https://github.com/ckeditor/ckeditor5-block-quote/commit/06de874)) |
||||
|
||||
### Other changes |
||||
|
||||
* Updated translations. ([5e23f86](https://github.com/ckeditor/ckeditor5-block-quote/commit/5e23f86)) |
||||
|
||||
|
||||
## 0.1.0 (2017-04-05) |
||||
|
||||
### Features |
||||
|
||||
* Introduced the block quote feature. Closes [#1](https://github.com/ckeditor/ckeditor5-block-quote/issues/1). ([239015b](https://github.com/ckeditor/ckeditor5-block-quote/commit/239015b)) |
@ -1,4 +0,0 @@ |
||||
Contributing |
||||
======================================== |
||||
|
||||
Information about contributing can be found at the following page: <https://github.com/ckeditor/ckeditor5/blob/master/CONTRIBUTING.md>. |
@ -1,23 +0,0 @@ |
||||
Software License Agreement |
||||
========================== |
||||
|
||||
**CKEditor 5 Block Quote Feature** – https://github.com/ckeditor/ckeditor5-paragraph <br> |
||||
Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. |
||||
|
||||
Licensed under the terms of any of the following licenses at your choice: |
||||
|
||||
* [GNU General Public License Version 2 or later (the "GPL")](http://www.gnu.org/licenses/gpl.html) |
||||
* [GNU Lesser General Public License Version 2.1 or later (the "LGPL")](http://www.gnu.org/licenses/lgpl.html) |
||||
* [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html) |
||||
|
||||
You are not required to, but if you want to explicitly declare the license you have chosen to be bound to when using, reproducing, modifying and distributing this software, just include a text file titled "legal.txt" in your version of this software, indicating your license choice. In any case, your choice will not restrict any recipient of your version of this software to use, reproduce, modify and distribute this software under any of the above licenses. |
||||
|
||||
Sources of Intellectual Property Included in CKEditor |
||||
----------------------------------------------------- |
||||
|
||||
Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission. |
||||
|
||||
Trademarks |
||||
---------- |
||||
|
||||
**CKEditor** is a trademark of [CKSource](http://cksource.com) Frederico Knabben. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders. |
@ -1,19 +0,0 @@ |
||||
CKEditor 5 block quote feature |
||||
======================================== |
||||
|
||||
[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) |
||||
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-block-quote.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-block-quote) |
||||
[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-block-quote.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-block-quote) |
||||
[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-block-quote/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-block-quote/coverage) |
||||
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-block-quote/status.svg)](https://david-dm.org/ckeditor/ckeditor5-block-quote) |
||||
[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-block-quote/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-block-quote?type=dev) |
||||
|
||||
This package implements block quote support for CKEditor 5. |
||||
|
||||
## Documentation |
||||
|
||||
See the [`@ckeditor/ckeditor5-block-quote` package](https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/api/block-quote.html) page in [CKEditor 5 documentation](https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/). |
||||
|
||||
## License |
||||
|
||||
Licensed under the GPL, LGPL and MPL licenses, at your choice. For full details about the license, please check the `LICENSE.md` file. |
@ -1,41 +0,0 @@ |
||||
{ |
||||
"requires": true, |
||||
"lockfileVersion": 1, |
||||
"dependencies": { |
||||
"husky": { |
||||
"version": "0.14.3", |
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", |
||||
"integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", |
||||
"requires": { |
||||
"is-ci": "1.0.10", |
||||
"normalize-path": "1.0.0", |
||||
"strip-indent": "2.0.0" |
||||
}, |
||||
"dependencies": { |
||||
"ci-info": { |
||||
"version": "1.1.1", |
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz", |
||||
"integrity": "sha512-vHDDF/bP9RYpTWtUhpJRhCFdvvp3iDWvEbuDbWgvjUrNGV1MXJrE0MPcwGtEled04m61iwdBLUIHZtDgzWS4ZQ==" |
||||
}, |
||||
"is-ci": { |
||||
"version": "1.0.10", |
||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", |
||||
"integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", |
||||
"requires": { |
||||
"ci-info": "1.1.1" |
||||
} |
||||
}, |
||||
"normalize-path": { |
||||
"version": "1.0.0", |
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", |
||||
"integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=" |
||||
}, |
||||
"strip-indent": { |
||||
"version": "2.0.0", |
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", |
||||
"integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,58 +0,0 @@ |
||||
{ |
||||
"name": "@ckeditor/ckeditor5-block-quote", |
||||
"version": "1.0.0-alpha.1", |
||||
"description": "Block quote feature for CKEditor 5.", |
||||
"keywords": [ |
||||
"ckeditor5", |
||||
"ckeditor5-feature" |
||||
], |
||||
"dependencies": { |
||||
"@ckeditor/ckeditor5-core": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-engine": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-ui": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-utils": "^1.0.0-alpha.1" |
||||
}, |
||||
"devDependencies": { |
||||
"@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-enter": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-essentials": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-image": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-list": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.1", |
||||
"@ckeditor/ckeditor5-typing": "^1.0.0-alpha.1", |
||||
"eslint": "^4.8.0", |
||||
"eslint-config-ckeditor5": "^1.0.6", |
||||
"husky": "^0.14.3", |
||||
"lint-staged": "^4.2.3" |
||||
}, |
||||
"engines": { |
||||
"node": ">=6.0.0", |
||||
"npm": ">=3.0.0" |
||||
}, |
||||
"author": "CKSource (http://cksource.com/)", |
||||
"license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)", |
||||
"homepage": "https://ckeditor5.github.io", |
||||
"bugs": "https://github.com/ckeditor/ckeditor5-block-quote/issues", |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "https://github.com/ckeditor/ckeditor5-block-quote.git" |
||||
}, |
||||
"files": [ |
||||
"lang", |
||||
"src", |
||||
"theme" |
||||
], |
||||
"scripts": { |
||||
"lint": "eslint --quiet '**/*.js'", |
||||
"precommit": "lint-staged" |
||||
}, |
||||
"lint-staged": { |
||||
"**/*.js": [ |
||||
"eslint --quiet" |
||||
] |
||||
}, |
||||
"eslintIgnore": [ |
||||
"src/lib/**", |
||||
"packages/**" |
||||
] |
||||
} |
@ -1,49 +0,0 @@ |
||||
/** |
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. |
||||
* For licensing, see LICENSE.md. |
||||
*/ |
||||
|
||||
/** |
||||
* @module block-quote/blockquote |
||||
*/ |
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; |
||||
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; |
||||
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; |
||||
|
||||
export default class OpTableWidget extends Plugin { |
||||
static get pluginName() { |
||||
return 'OP-Table'; |
||||
} |
||||
|
||||
init() { |
||||
const editor = this.editor; |
||||
const data = editor.data; |
||||
const schema = editor.document.schema; |
||||
const editing = editor.editing; |
||||
|
||||
schema.registerItem( 'table' ); |
||||
schema.allow( { name: 'table', inside: '$root' } ); |
||||
// thead
|
||||
schema.allow( { name: 'thead', inside: 'table' } ); |
||||
schema.allow( { name: 'tr', inside: 'thead' } ); |
||||
schema.allow( { name: 'th', inside: 'tr' } ); |
||||
|
||||
// tbody
|
||||
schema.allow( { name: 'tbody', inside: 'table' } ); |
||||
schema.allow( { name: 'tr', inside: 'tbody' } ); |
||||
schema.allow( { name: 'td', inside: 'tr' } ); |
||||
// schema.allow( { name: '$block', inside: 'opTable' } );
|
||||
|
||||
|
||||
buildModelConverter().for( data.modelToView, editing.modelToView ) |
||||
.fromElement( 'opTable' ) |
||||
.toElement('div') |
||||
|
||||
// Build converter from view to model for data pipeline.
|
||||
buildViewConverter().for( data.viewToModel ) |
||||
.fromElement( 'div' ) |
||||
.fromAttribute( 'class', 'op-ckeditor-widget--table') |
||||
.toElement('opTable'); |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
'use strict'; |
||||
|
||||
const path = require( 'path' ); |
||||
const postcssImport = require( 'postcss-import' ); |
||||
const postcssCssnext = require( 'postcss-cssnext' ); |
||||
//const CKThemeImporter = require( './ck-theme-importer' );
|
||||
|
||||
module.exports = { |
||||
plugins: [ |
||||
postcssImport(), |
||||
postcssCssnext() |
||||
] |
||||
}; |
@ -0,0 +1,29 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES6", |
||||
"allowJs": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"module": "ES6", |
||||
"moduleResolution": "node", |
||||
"removeComments": true, |
||||
"preserveConstEnums": true, |
||||
"sourceMap": true, |
||||
"noEmitOnError": false, |
||||
// Increase strictness |
||||
"noImplicitAny": false, |
||||
"noImplicitThis": true, |
||||
"noImplicitReturns": true, |
||||
"noFallthroughCasesInSwitch": true, |
||||
"strictNullChecks": true, |
||||
"skipLibCheck": true, |
||||
"baseUrl": ".", |
||||
"paths": { |
||||
"core-components/*": ["../app/components/*"], |
||||
"op-ckeditor/*": ["./*"] |
||||
} |
||||
}, |
||||
"compileOnSave": false, |
||||
"exclude": [ |
||||
"node_modules" |
||||
] |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,147 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
var webpack = require('webpack'); |
||||
var fs = require('fs'); |
||||
var path = require('path'); |
||||
var _ = require('lodash'); |
||||
var autoprefixer = require('autoprefixer'); |
||||
|
||||
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' ); |
||||
var CleanWebpackPlugin = require('clean-webpack-plugin'); |
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin'); |
||||
|
||||
var mode = (process.env['RAILS_ENV'] || 'production').toLowerCase(); |
||||
var uglify = (mode !== 'development'); |
||||
|
||||
var node_root = path.resolve(__dirname, 'node_modules'); |
||||
var output_root = path.resolve(__dirname, '..', 'app', 'assets', 'javascripts'); |
||||
var bundle_output = path.resolve(output_root, 'editor') |
||||
|
||||
function getWebpackCKEConfig() { |
||||
config = { |
||||
entry: { |
||||
ckeditor: [path.resolve(__dirname, 'ckeditor', 'ckeditor.ts')] |
||||
}, |
||||
|
||||
module: { |
||||
rules: [ |
||||
{ |
||||
test: /\.tsx?$/, |
||||
include: [ |
||||
path.resolve(__dirname, 'ckeditor'), |
||||
path.resolve(__dirname, 'app'), |
||||
], |
||||
use: [ |
||||
{ |
||||
loader: 'ts-loader', |
||||
options: { |
||||
logLevel: 'info', |
||||
configFile: path.resolve(__dirname, 'ckeditor', 'tsconfig.json') |
||||
} |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
// Or /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg$/ if you want to limit this loader
|
||||
// to CKEditor 5's icons only.
|
||||
test: /\.svg$/, |
||||
|
||||
use: [ 'raw-loader' ] |
||||
}, |
||||
{ |
||||
// Or /ckeditor5-[^/]+\/theme\/[^/]+\.scss$/ if you want to limit this loader
|
||||
// to CKEditor 5's theme only.
|
||||
test: /\.css$/, |
||||
|
||||
use: [ |
||||
'style-loader', |
||||
{ |
||||
loader: 'postcss-loader', |
||||
options: { |
||||
config: { path: path.resolve(__dirname, 'ckeditor', 'postcss.config.js') } |
||||
} |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
// Or /ckeditor5-[^/]+\/theme\/[^/]+\.scss$/ if you want to limit this loader
|
||||
// to CKEditor 5's theme only.
|
||||
test: /\.scss$/, |
||||
|
||||
use: [ |
||||
'style-loader', |
||||
{ |
||||
loader: 'css-loader', |
||||
options: { |
||||
minimize: true |
||||
} |
||||
}, |
||||
'sass-loader' |
||||
] |
||||
} |
||||
] |
||||
}, |
||||
|
||||
output: { |
||||
path: bundle_output, |
||||
filename: 'openproject-[name].js', |
||||
library: '[name]' |
||||
}, |
||||
|
||||
resolve: { |
||||
modules: ['node_modules'], |
||||
extensions: ['.ts', '.tsx', '.js'], |
||||
alias: _.merge({ |
||||
'core-components': path.resolve(__dirname, 'app', 'components'), |
||||
'op-ckeditor': path.resolve(__dirname, 'ckeditor'), |
||||
}) |
||||
}, |
||||
|
||||
plugins: [ |
||||
|
||||
|
||||
// Editor i18n TODO
|
||||
new CKEditorWebpackPlugin({ |
||||
// See https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/features/ui-language.html
|
||||
languages: [ 'en' ] |
||||
}), |
||||
|
||||
|
||||
// Clean the output directory
|
||||
new CleanWebpackPlugin(['editor'], { |
||||
root: output_root, |
||||
verbose: true |
||||
}) |
||||
] |
||||
}; |
||||
|
||||
return config; |
||||
} |
||||
|
||||
module.exports = getWebpackCKEConfig; |
@ -0,0 +1,189 @@ |
||||
# Textile to Markdown converter |
||||
# Based on redmine_convert_textile_to_markown |
||||
# https://github.com/Ecodev/redmine_convert_textile_to_markown |
||||
# |
||||
# Original license: |
||||
# Copyright (c) 2016 |
||||
# |
||||
# 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. |
||||
|
||||
require 'open3' |
||||
|
||||
module OpenProject::TextFormatting::Formatters |
||||
module Markdown |
||||
class TextileConverter |
||||
attr_reader :src, :dst |
||||
|
||||
def initialize |
||||
end |
||||
|
||||
|
||||
def run! |
||||
puts 'Starting conversion of Textile fields to CommonMark+GFM.' |
||||
|
||||
ActiveRecord::Base.transaction do |
||||
converters.each do |handler| |
||||
handler.call! |
||||
end |
||||
end |
||||
|
||||
puts "\n-- Completed --" |
||||
ensure |
||||
cleanup |
||||
end |
||||
|
||||
private |
||||
|
||||
def converters |
||||
[ |
||||
method(:convert_settings) |
||||
method(:convert_models) |
||||
] |
||||
end |
||||
|
||||
def cleanup |
||||
src.close |
||||
dst.close |
||||
end |
||||
|
||||
def convert_settings |
||||
print 'Converting settings ' |
||||
Setting.welcome_text = convert_textile_to_markdown(Setting.welcome_text) |
||||
print '.' |
||||
|
||||
Setting.registration_footer = Setting.registration_footer.dup.tap do |footer| |
||||
footer.transform_values { |val| convert_textile_to_markdown(val) } |
||||
print '.' |
||||
end |
||||
|
||||
puts 'done' |
||||
end |
||||
|
||||
def convert_models |
||||
models_to_convert.each do |the_class, attributes| |
||||
print "#{the_class.name} " |
||||
|
||||
# Iterate in batches to avoid plucking too much |
||||
the_class.in_batches(of: 200) do |relation| |
||||
relation.pluck(:id, *attributes).each do |values| |
||||
# Zip converted texts into |
||||
# { attr_a: textile, ... } |
||||
converted = values.drop(1).map(&method(:convert_textile_to_markdown)) |
||||
update_hash = Hash[attributes.zip(converted)] |
||||
the_class.where(id: values.first).update_all(update_hash) |
||||
|
||||
print '.' |
||||
end |
||||
end |
||||
puts 'done' |
||||
end |
||||
end |
||||
|
||||
def convert_textile_to_markdown(textile) |
||||
return '' unless textile.present? |
||||
|
||||
# Redmine support @ inside inline code marked with @ (such as "@git@github.com@"), but not pandoc. |
||||
# So we inject a placeholder that will be replaced later on with a real backtick. |
||||
tag_code = 'pandoc-unescaped-single-backtick' |
||||
textile.gsub!(/@([\S]+@[\S]+)@/, tag_code + '\\1' + tag_code) |
||||
|
||||
# Drop table colspan/rowspan notation ("|\2." or "|/2.") because pandoc does not support it |
||||
# See https://github.com/jgm/pandoc/issues/22 |
||||
textile.gsub!(/\|[\/\\]\d\. /, '| ') |
||||
|
||||
# Drop table alignement notation ("|>." or "|<." or "|=.") because pandoc does not support it |
||||
# See https://github.com/jgm/pandoc/issues/22 |
||||
textile.gsub!(/\|[<>=]\. /, '| ') |
||||
|
||||
# Move the class from <code> to <pre> so pandoc can generate a code block with correct language |
||||
textile.gsub!(/(<pre)(><code)( class="[^"]*")(>)/, '\\1\\3\\2\\4') |
||||
|
||||
# Remove the <code> directly inside <pre>, because pandoc would incorrectly preserve it |
||||
textile.gsub!(/(<pre[^>]*>)<code>/, '\\1') |
||||
textile.gsub!(/<\/code>(<\/pre>)/, '\\1') |
||||
|
||||
# Inject a class in all <pre> that do not have a blank line before them |
||||
# This is to force pandoc to use fenced code block (```) otherwise it would |
||||
# use indented code block and would very likely need to insert an empty HTML |
||||
# comment "<!-- -->" (see http://pandoc.org/README.html#ending-a-list) |
||||
# which are unfortunately not supported by Redmine (see http://www.redmine.org/issues/20497) |
||||
tag_fenced_code_block = 'force-pandoc-to-ouput-fenced-code-block' |
||||
textile.gsub!(/([^\n]<pre)(>)/, "\\1 class=\"#{tag_fenced_code_block}\"\\2") |
||||
|
||||
# Force <pre> to have a blank line before them |
||||
# Without this fix, a list of items containing <pre> would not be interpreted as a list at all. |
||||
textile.gsub!(/([^\n])(<pre)/, "\\1\n\n\\2") |
||||
|
||||
# Some malformed textile content make pandoc run extremely slow, |
||||
# so we convert it to proper textile before hitting pandoc |
||||
# see https://github.com/jgm/pandoc/issues/3020 |
||||
textile.gsub!(/- # (\d+)/, "* \\1") |
||||
|
||||
command = %w(pandoc --wrap=preserve -f textile -t gfm) |
||||
markdown, stderr_str, status = Open3.capture3(*command, stdin_data: textile) |
||||
|
||||
raise 'Pandoc failed: #{stderr.read}' unless status.success? |
||||
|
||||
# Remove the \ pandoc puts before * and > at begining of lines |
||||
markdown.gsub!(/^((\\[*>])+)/) { $1.gsub("\\", "") } |
||||
|
||||
# Add a blank line before lists |
||||
markdown.gsub!(/^([^*].*)\n\*/, "\\1\n\n*") |
||||
|
||||
# Remove the injected tag |
||||
markdown.gsub!(' ' + tag_fenced_code_block, '') |
||||
|
||||
# Replace placeholder with real backtick |
||||
markdown.gsub!(tag_code, '`') |
||||
|
||||
# Un-escape Redmine link syntax to wiki pages |
||||
markdown.gsub!('\[\[', '[[') |
||||
markdown.gsub!('\]\]', ']]') |
||||
|
||||
# Un-escape Redmine quotation mark "> " that pandoc is not aware of |
||||
markdown.gsub!(/(^|\n)> /, "\n> ") |
||||
|
||||
return markdown |
||||
end |
||||
|
||||
def models_to_convert |
||||
{ |
||||
Announcement => [:text], |
||||
AttributeHelpText => [:help_text], |
||||
Comment => [:text], |
||||
WikiContent => [:text], |
||||
WorkPackage => [:description], |
||||
Message => [:content], |
||||
News => [:description], |
||||
Document => [:description], |
||||
Project => [:description], |
||||
Journal => [:notes], |
||||
AttachmentJournal => [:text], |
||||
MessageJournal => [:content], |
||||
WikiContentJournal => [:text], |
||||
WorkPackageJournal => [:description], |
||||
## TODO |
||||
# CF Long text values |
||||
# Documents |
||||
# Meetings |
||||
} |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,514 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module OpenProject::TextFormatting::Formatters |
||||
module Textile |
||||
class LegacyTextFormatting |
||||
include Redmine::WikiFormatting::Macros::Definitions |
||||
include ActionView::Helpers::SanitizeHelper |
||||
include Redmine::I18n |
||||
# used for the work package quick links |
||||
include WorkPackagesHelper |
||||
# Used for escaping helper 'h()' |
||||
include ERB::Util |
||||
# Rails helper |
||||
include ActionView::Helpers::TagHelper |
||||
include ActionView::Helpers::UrlHelper |
||||
include ActionView::Helpers::TextHelper |
||||
# For route path helpers |
||||
include OpenProject::ObjectLinking |
||||
include OpenProject::StaticRouting::UrlHelpers |
||||
# Truncation |
||||
include OpenProject::TextFormatting::Truncation |
||||
|
||||
def initialize(project) |
||||
@project = project |
||||
end |
||||
|
||||
def controller; end |
||||
|
||||
# Formats text according to system settings. |
||||
# 2 ways to call this method: |
||||
# * with a String: format_text(text, options) |
||||
# * with an object and one of its attribute: format_text(issue, :description, options) |
||||
def format_text(text, options) |
||||
edit = !!options[:edit] |
||||
# don't return html in edit mode when textile or text formatting is enabled |
||||
return text if edit |
||||
|
||||
object = options[:object] |
||||
project = options[:project] |
||||
only_path = options.delete(:only_path) != false |
||||
|
||||
# offer 'plain' as readable version for 'no formatting' to callers |
||||
format = options.delete(:format) { :textile } |
||||
text = OpenProject::TextFormatting::Formatters.formatter_for(format).new(text).to_html |
||||
|
||||
# TODO: transform modifications into WikiFormatting Helper, or at least ask the helper if he wants his stuff to be modified |
||||
@parsed_headings = [] |
||||
text = parse_non_pre_blocks(text) { |text| |
||||
[:execute_macros, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings, :parse_relative_urls].each do |method_name| |
||||
send method_name, text, project, object, options[:attribute], only_path, options |
||||
end |
||||
} |
||||
|
||||
if @parsed_headings.any? |
||||
replace_toc(text, @parsed_headings, options) |
||||
end |
||||
|
||||
escape_non_macros(text) |
||||
text.html_safe |
||||
end |
||||
|
||||
## |
||||
# Escape double curly braces after macro expansion. |
||||
# This will avoid arbitrary angular expressions to be evaluated in |
||||
# formatted text marked html_safe. |
||||
def escape_non_macros(text) |
||||
text.gsub!(/\{\{(?! \$root\.DOUBLE_LEFT_CURLY_BRACE)/, '{{ $root.DOUBLE_LEFT_CURLY_BRACE }}') |
||||
end |
||||
|
||||
def parse_non_pre_blocks(text) |
||||
s = StringScanner.new(text) |
||||
tags = [] |
||||
parsed = '' |
||||
while !s.eos? |
||||
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
||||
text = s[1] |
||||
full_tag = s[2] |
||||
closing = s[3] |
||||
tag = s[4] |
||||
if tags.empty? |
||||
yield text |
||||
end |
||||
parsed << text |
||||
if tag |
||||
if closing |
||||
if tags.last == tag.downcase |
||||
tags.pop |
||||
end |
||||
else |
||||
tags << tag.downcase |
||||
end |
||||
parsed << full_tag |
||||
end |
||||
end |
||||
# Close any non closing tags |
||||
while tag = tags.pop |
||||
parsed << "</#{tag}>" |
||||
end |
||||
parsed |
||||
end |
||||
|
||||
|
||||
MACROS_RE = / |
||||
(!)? # escaping |
||||
( |
||||
\{\{ # opening tag |
||||
([\w]+) # macro name |
||||
(\(([^\}]*)\))? # optional arguments |
||||
\}\} # closing tag |
||||
) |
||||
/x unless const_defined?(:MACROS_RE) |
||||
|
||||
# Macros substitution |
||||
def execute_macros(text, project, obj, _attr, _only_path, options) |
||||
return if !!options[:edit] |
||||
text.gsub!(MACROS_RE) do |
||||
esc = $1 |
||||
all = $2 |
||||
macro = $3 |
||||
args = ($5 || '').split(',').each(&:strip!) |
||||
if esc.nil? |
||||
begin |
||||
exec_macro(macro, obj, args, view: self, project: project) |
||||
rescue => e |
||||
"<span class=\"flash error macro-unavailable permanent\">\ |
||||
#{::I18n.t(:macro_execution_error, macro_name: macro)} (#{e})\ |
||||
</span>".squish |
||||
rescue NotImplementedError |
||||
"<span class=\"flash error macro-unavailable permanent\">\ |
||||
#{::I18n.t(:macro_unavailable, macro_name: macro)}\ |
||||
</span>".squish |
||||
end || all |
||||
else |
||||
all |
||||
end |
||||
end |
||||
end |
||||
|
||||
RELATIVE_LINK_RE = %r{ |
||||
<a |
||||
(?: |
||||
(\shref= |
||||
(?: # the href and link |
||||
(?:'(\/[^>]+?)')| |
||||
(?:"(\/[^>]+?)") |
||||
) |
||||
)| |
||||
[^>] |
||||
)* |
||||
> |
||||
[^<]*?<\/a> # content and closing link tag. |
||||
}x unless const_defined?(:RELATIVE_LINK_RE) |
||||
|
||||
def parse_relative_urls(text, _project, _obj, _attr, only_path, _options) |
||||
return if only_path |
||||
text.gsub!(RELATIVE_LINK_RE) do |m| |
||||
href = $1 |
||||
relative_url = $2 || $3 |
||||
next m unless href.present? |
||||
if defined?(request) && request.present? |
||||
# we have a request! |
||||
protocol = request.protocol |
||||
host_with_port = request.host_with_port |
||||
elsif @controller |
||||
# use the same methods as url_for in the Mailer |
||||
url_opts = @controller.class.default_url_options |
||||
next m unless url_opts && url_opts[:protocol] && url_opts[:host] |
||||
protocol = "#{url_opts[:protocol]}://" |
||||
host_with_port = url_opts[:host] |
||||
else |
||||
next m |
||||
end |
||||
m.sub href, " href=\"#{protocol}#{host_with_port}#{relative_url}\"" |
||||
end |
||||
end |
||||
|
||||
def parse_inline_attachments(text, _project, obj, _attr, only_path, options) |
||||
# when using an image link, try to use an attachment, if possible |
||||
if options[:attachments] || (obj && obj.respond_to?(:attachments)) |
||||
attachments = nil |
||||
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
||||
filename = $1.downcase |
||||
ext = $2 |
||||
alt = $3 |
||||
alttext = $4 |
||||
attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse |
||||
# search for the picture in attachments |
||||
if found = attachments.detect { |att| att.filename.downcase == filename } |
||||
image_url = url_for only_path: only_path, controller: '/attachments', action: 'download', id: found |
||||
desc = found.description.to_s.gsub('"', '') |
||||
if !desc.blank? && alttext.blank? |
||||
alt = " title=\"#{desc}\" alt=\"#{desc}\"" |
||||
end |
||||
"src=\"#{image_url}\"#{alt}" |
||||
else |
||||
m |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
# Wiki links |
||||
# |
||||
# Examples: |
||||
# [[mypage]] |
||||
# [[mypage|mytext]] |
||||
# wiki links can refer other project wikis, using project name or identifier: |
||||
# [[project:]] -> wiki starting page |
||||
# [[project:|mytext]] |
||||
# [[project:mypage]] |
||||
# [[project:mypage|mytext]] |
||||
def parse_wiki_links(text, project, _obj, _attr, only_path, options) |
||||
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |_m| |
||||
link_project = project |
||||
esc = $1 |
||||
all = $2 |
||||
page = $3 |
||||
title = $5 |
||||
if esc.nil? |
||||
if page =~ /\A([^\:]+)\:(.*)\z/ |
||||
link_project = Project.find_by(identifier: $1) || Project.find_by(name: $1) |
||||
page = $2 |
||||
title ||= $1 if page.blank? |
||||
end |
||||
|
||||
if link_project && link_project.wiki |
||||
# extract anchor |
||||
anchor = nil |
||||
if page =~ /\A(.+?)\#(.+)\z/ |
||||
page = $1 |
||||
anchor = $2 |
||||
end |
||||
# Unescape the escaped entities from textile |
||||
page = CGI.unescapeHTML(page) |
||||
# check if page exists |
||||
wiki_page = link_project.wiki.find_page(page) |
||||
wiki_title = wiki_page.nil? ? page : wiki_page.title |
||||
url = case options[:wiki_links] |
||||
when :local; |
||||
"#{title}.html" |
||||
when :anchor; |
||||
"##{title}" # used for single-file wiki export |
||||
else |
||||
wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug |
||||
url_for(only_path: only_path, controller: '/wiki', action: 'show', project_id: link_project, id: wiki_page_id, anchor: anchor) |
||||
end |
||||
link_to(h(title || wiki_title), url, class: ('wiki-page' + (wiki_page ? '' : ' new'))) |
||||
else |
||||
# project or wiki doesn't exist |
||||
all |
||||
end |
||||
else |
||||
all |
||||
end |
||||
end |
||||
end |
||||
|
||||
# Redmine links |
||||
# |
||||
# Examples: |
||||
# Issues: |
||||
# #52 -> Link to issue #52 |
||||
# Changesets: |
||||
# r52 -> Link to revision 52 |
||||
# commit:a85130f -> Link to scmid starting with a85130f |
||||
# Documents: |
||||
# document#17 -> Link to document with id 17 |
||||
# document:Greetings -> Link to the document with title "Greetings" |
||||
# document:"Some document" -> Link to the document with title "Some document" |
||||
# Versions: |
||||
# version#3 -> Link to version with id 3 |
||||
# version:1.0.0 -> Link to version named "1.0.0" |
||||
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
||||
# Attachments: |
||||
# attachment:file.zip -> Link to the attachment of the current object named file.zip |
||||
# Source files: |
||||
# source:some/file -> Link to the file located at /some/file in the project's repository |
||||
# source:some/file@52 -> Link to the file's revision 52 |
||||
# source:some/file#L120 -> Link to line 120 of the file |
||||
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
||||
# export:some/file -> Force the download of the file |
||||
# Forum messages: |
||||
# message#1218 -> Link to message with id 1218 |
||||
# |
||||
# Links can refer other objects from other projects, using project identifier: |
||||
# identifier:r52 |
||||
# identifier:document:"Some document" |
||||
# identifier:version:1.0.0 |
||||
# identifier:source:some/file |
||||
def parse_redmine_links(text, project, obj, attr, only_path, options) |
||||
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|version|commit|source|export|message|project|user)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |_m| |
||||
leading = $1 |
||||
esc = $2 |
||||
project_prefix = $3 |
||||
project_identifier = $4 |
||||
prefix = $5 |
||||
sep = $7 || $9 |
||||
identifier = $8 || $10 |
||||
link = nil |
||||
if project_identifier |
||||
project = Project.visible.find_by(identifier: project_identifier) |
||||
end |
||||
if esc.nil? |
||||
if prefix.nil? && sep == 'r' |
||||
# project.changesets.visible raises an SQL error because of a double join on repositories |
||||
if project && project.repository && (changeset = Changeset.visible.find_by(repository_id: project.repository.id, revision: identifier)) |
||||
link = link_to(h("#{project_prefix}r#{identifier}"), { only_path: only_path, controller: '/repositories', action: 'revision', project_id: project, rev: changeset.revision }, |
||||
class: 'changeset', |
||||
title: truncate_single_line(changeset.comments, length: 100)) |
||||
end |
||||
elsif sep == '#' |
||||
oid = identifier.to_i |
||||
case prefix |
||||
when nil |
||||
if work_package = WorkPackage.visible |
||||
.includes(:status) |
||||
.references(:statuses) |
||||
.find_by(id: oid) |
||||
link = link_to("##{oid}", |
||||
work_package_path_or_url(id: oid, only_path: only_path), |
||||
class: work_package_css_classes(work_package), |
||||
title: "#{truncate(work_package.subject, length: 100)} (#{work_package.status.try(:name)})") |
||||
end |
||||
when 'version' |
||||
if version = Version.visible.find_by(id: oid) |
||||
link = link_to h(version.name), { only_path: only_path, controller: '/versions', action: 'show', id: version }, |
||||
class: 'version' |
||||
end |
||||
when 'message' |
||||
if message = Message.visible.includes(:parent).find_by(id: oid) |
||||
link = link_to_message(message, { only_path: only_path }, class: 'message') |
||||
end |
||||
when 'project' |
||||
if p = Project.visible.find_by(id: oid) |
||||
link = link_to_project(p, { only_path: only_path }, class: 'project') |
||||
end |
||||
when 'user' |
||||
if user = User.in_visible_project.find_by(id: oid) |
||||
link = link_to_user(user, class: 'user-mention') |
||||
end |
||||
end |
||||
elsif sep == '##' |
||||
oid = identifier.to_i |
||||
if work_package = WorkPackage.visible |
||||
.includes(:status) |
||||
.references(:statuses) |
||||
.find_by(id: oid) |
||||
link = work_package_quick_info(work_package, only_path: only_path) |
||||
end |
||||
elsif sep == '###' |
||||
oid = identifier.to_i |
||||
work_package = WorkPackage.visible |
||||
.includes(:status) |
||||
.references(:statuses) |
||||
.find_by(id: oid) |
||||
if work_package && obj && !(attr == :description && obj.id == work_package.id) |
||||
link = work_package_quick_info_with_description(work_package, only_path: only_path) |
||||
end |
||||
elsif sep == ':' |
||||
# removes the double quotes if any |
||||
name = identifier.gsub(%r{\A"(.*)"\z}, '\\1') |
||||
case prefix |
||||
when 'version' |
||||
if project && version = project.versions.visible.find_by(name: name) |
||||
link = link_to h(version.name), { only_path: only_path, controller: '/versions', action: 'show', id: version }, |
||||
class: 'version' |
||||
end |
||||
when 'commit' |
||||
if project && project.repository && (changeset = Changeset.visible.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{name}%"]).first) |
||||
link = link_to h("#{project_prefix}#{name}"), { only_path: only_path, controller: '/repositories', action: 'revision', project_id: project, rev: changeset.identifier }, |
||||
class: 'changeset', |
||||
title: truncate_single_line(changeset.comments, length: 100) |
||||
end |
||||
when 'source', 'export' |
||||
if project && project.repository && User.current.allowed_to?(:browse_repository, project) |
||||
name =~ %r{\A[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?\z} |
||||
path = $1 |
||||
rev = $3 |
||||
anchor = $5 |
||||
link = link_to h("#{project_prefix}#{prefix}:#{name}"), { controller: '/repositories', action: 'entry', project_id: project, |
||||
path: path.to_s, |
||||
rev: rev, |
||||
anchor: anchor, |
||||
format: (prefix == 'export' ? 'raw' : nil) }, |
||||
class: (prefix == 'export' ? 'source download' : 'source') |
||||
end |
||||
when 'attachment' |
||||
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
||||
if attachments && attachment = attachments.detect { |a| a.filename == name } |
||||
link = link_to h(attachment.filename), { only_path: only_path, controller: '/attachments', action: 'download', id: attachment }, |
||||
class: 'attachment' |
||||
end |
||||
when 'project' |
||||
p = Project |
||||
.visible |
||||
.where(['projects.identifier = :s OR LOWER(projects.name) = :s', |
||||
{ s: name.downcase }]) |
||||
.first |
||||
if p |
||||
link = link_to_project(p, { only_path: only_path }, class: 'project') |
||||
end |
||||
when 'user' |
||||
if user = User.in_visible_project.find_by(login: name) |
||||
link = link_to_user(user, class: 'user-mention') |
||||
end |
||||
end |
||||
end |
||||
end |
||||
leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}") |
||||
end |
||||
end |
||||
|
||||
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE) |
||||
|
||||
# Headings and TOC |
||||
# Adds ids and links to headings unless options[:headings] is set to false |
||||
def parse_headings(text, _project, _obj, _attr, _only_path, options) |
||||
return if options[:headings] == false |
||||
|
||||
text.gsub!(HEADING_RE) do |
||||
level = $1.to_i |
||||
attrs = $2 |
||||
content = $3 |
||||
item = strip_tags(content).strip |
||||
tocitem = strip_tags(content.gsub(/<br \/>/, ' ')) |
||||
anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
||||
@parsed_headings << [level, anchor, tocitem] |
||||
url = full_url(anchor, options[:request]) |
||||
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"#{url}\" class=\"wiki-anchor\">¶</a></h#{level}>" |
||||
end |
||||
end |
||||
|
||||
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
||||
|
||||
# Renders the TOC with given headings |
||||
def replace_toc(text, headings, options) |
||||
text.gsub!(TOC_RE) do |
||||
if headings.empty? |
||||
'' |
||||
else |
||||
div_class = 'toc' |
||||
div_class << ' right' if $1 == '>' |
||||
div_class << ' left' if $1 == '<' |
||||
out = "<fieldset class='form--fieldset -collapsible'>" |
||||
out << "<legend class='form--fieldset-legend' title='" + |
||||
l(:description_toc_toggle) + |
||||
"' onclick='toggleFieldset(this);'> |
||||
<a href='javascript:'> |
||||
#{l(:label_table_of_contents)} |
||||
</a> |
||||
</legend><div>" |
||||
out << "<ul class=\"#{div_class}\"><li>" |
||||
root = headings.map(&:first).min |
||||
current = root |
||||
started = false |
||||
headings.each do |level, anchor, item| |
||||
if level > current |
||||
out << '<ul><li>' * (level - current) |
||||
elsif level < current |
||||
out << "</li></ul>\n" * (current - level) + '</li><li>' |
||||
elsif started |
||||
out << '</li><li>' |
||||
end |
||||
url = full_url(anchor, options[:request]) |
||||
out << "<a href=\"#{url}\">#{item}</a>" |
||||
current = level |
||||
started = true |
||||
end |
||||
out << '</li></ul>' * (current - root) |
||||
out << '</li></ul>' |
||||
out << '</div></fieldset>' |
||||
end |
||||
end |
||||
end |
||||
|
||||
# |
||||
# displays the current url plus an optional anchor |
||||
# |
||||
def full_url(anchor_name = '', current_request = nil) |
||||
return "##{anchor_name}" if current_request.nil? |
||||
current = current_request.original_fullpath |
||||
return current if anchor_name.blank? |
||||
"#{current}##{anchor_name}" |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,143 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'redcloth3' |
||||
|
||||
module OpenProject::TextFormatting::Formatters |
||||
module Textile |
||||
class RedclothWrapper < RedCloth3 |
||||
include ERB::Util |
||||
include ActionView::Helpers::TagHelper |
||||
|
||||
# auto_link rule after textile rules so that it doesn't break !image_url! tags |
||||
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] |
||||
|
||||
def initialize(*args) |
||||
super |
||||
|
||||
self.hard_breaks = true |
||||
self.no_span_caps = true |
||||
self.filter_styles = true |
||||
end |
||||
|
||||
def to_html(*_rules) |
||||
@toc = [] |
||||
super(*RULES).to_s |
||||
end |
||||
|
||||
private |
||||
|
||||
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. |
||||
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> |
||||
def hard_break(text) |
||||
text.gsub!(/(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, '\\1<br />') if hard_breaks |
||||
end |
||||
|
||||
# Patch to add code highlighting support to RedCloth |
||||
def smooth_offtags(text) |
||||
unless @pre_list.empty? |
||||
## replace <pre> content |
||||
text.gsub!(/<redpre#(\d+)>/) do |
||||
content = @pre_list[$1.to_i] |
||||
if content.match(/<code\s+class="(\w+)">\s?(.+)/m) |
||||
content = "<code class=\"#{$1} CodeRay\">" + |
||||
Redmine::SyntaxHighlighting.highlight_by_language($2, $1) |
||||
end |
||||
content |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
def auto_link_regexp |
||||
@auto_link_regexp ||= begin |
||||
%r{ |
||||
( # leading text |
||||
<\w+.*?>| # leading HTML tag, or |
||||
[^=<>!:'"/]| # leading punctuation, or |
||||
\{\{\w+\(| # inside a macro? |
||||
^ # beginning of line |
||||
) |
||||
( |
||||
(?:https?://)| # protocol spec, or |
||||
(?:s?ftps?://)| |
||||
(?:www\.) # www.* |
||||
) |
||||
( |
||||
(\S+?) # url |
||||
(\/)? # slash |
||||
) |
||||
((?:>)?|[^\w\=\/;\(\)]*?) # post |
||||
(?=<|\s|$) |
||||
}x |
||||
end |
||||
end |
||||
|
||||
# Turns all urls into clickable links (code from Rails). |
||||
def inline_auto_link(text) |
||||
text.gsub!(auto_link_regexp) do |
||||
all = $& |
||||
leading = $1 |
||||
proto = $2 |
||||
url = $3 |
||||
post = $6 |
||||
if url.nil? || leading =~ /<a\s/i || leading =~ /![<>=]?/ || leading =~ /\{\{\w+\(/ |
||||
# don't replace URLs that are already linked |
||||
# and URLs prefixed with ! !> !< != (textile images) |
||||
all |
||||
else |
||||
# Idea below : an URL with unbalanced parethesis and |
||||
# ending by ')' is put into external parenthesis |
||||
if url[-1] == ?) and ((url.count('(') - url.count(')')) < 0) |
||||
url = url[0..-2] # discard closing parenth from url |
||||
post = ')' + post # add closing parenth to post |
||||
end |
||||
tag = content_tag('a', |
||||
proto + url, |
||||
href: "#{proto == 'www.' ? 'http://www.' : proto}#{url}", |
||||
class: 'external icon-context icon-copy') |
||||
%(#{leading}#{tag}#{post}) |
||||
end |
||||
end |
||||
end |
||||
|
||||
# Turns all email addresses into clickable links (code from Rails). |
||||
def inline_auto_mailto(text) |
||||
text.gsub!(/((?<!user:")\b[\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do |
||||
mail = $1 |
||||
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) |
||||
mail |
||||
else |
||||
content_tag('a', mail, href: "mailto:#{mail}", class: 'email') |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
|
||||
# Textile to Markdown converter |
||||
# Based on redmine_convert_textile_to_markown |
||||
# https://github.com/Ecodev/redmine_convert_textile_to_markown |
||||
# |
||||
# Original license: |
||||
# Copyright (c) 2016 |
||||
# |
||||
# 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. |
||||
|
||||
namespace :markdown do |
||||
task :convert_from_textile => :environment do |
||||
|
||||
warning = <<~EOS |
||||
**WARNING** |
||||
THIS IS NOT REVERSIBLE. |
||||
Ensure you have backed up your installation before running this task. |
||||
|
||||
This rake task will modify EVERY formattable textile field in your database. |
||||
It uses pandoc to convert each textile field to GFM-Markdown. |
||||
EOS |
||||
|
||||
printf "#{warning}\nPress 'y' to continue: " |
||||
prompt = STDIN.gets.chomp |
||||
exit(1) unless prompt == 'y' |
||||
|
||||
converter = OpenProject::TextFormatting::Formatters::Markdown::TextileConverter.new |
||||
converter.run! |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
require_relative './work_package_field' |
||||
|
||||
class WorkPackageEditorField < WorkPackageField |
||||
|
||||
def input_selector |
||||
'div.op-ckeditor-wrapper' |
||||
end |
||||
|
||||
def expect_save_button(enabled: true) |
||||
if enabled |
||||
expect(field_container).to have_no_selector("#{control_link}[disabled]") |
||||
else |
||||
expect(field_container).to have_selector("#{control_link}[disabled]") |
||||
end |
||||
end |
||||
|
||||
def expect_value(value) |
||||
expect(input_element.text).to eq(value) |
||||
end |
||||
|
||||
def save! |
||||
submit_by_click |
||||
end |
||||
|
||||
def submit_by_click |
||||
target = field_container.find(control_link) |
||||
scroll_to_element(target) |
||||
target.click |
||||
end |
||||
|
||||
def submit_by_keyboard |
||||
input_element.native.send_keys :tab |
||||
end |
||||
|
||||
def cancel_by_click |
||||
target = field_container.find(control_link(:cancel)) |
||||
scroll_to_element(target) |
||||
target.click |
||||
end |
||||
|
||||
def field_type |
||||
input_selector |
||||
end |
||||
|
||||
def control_link(action = :save) |
||||
raise 'Invalid link' unless [:save, :cancel].include?(action) |
||||
".inplace-edit--control--#{action}" |
||||
end |
||||
end |
Loading…
Reference in new issue