Merge branch 'develop' into fix_tab
@ -41,7 +41,8 @@ try {
|
||||
"./src/public/robots.txt",
|
||||
"./src/public/fonts",
|
||||
"./src/public/stylesheets",
|
||||
"./src/public/translations"
|
||||
"./src/public/translations",
|
||||
"./packages/turndown-plugin-gfm/src"
|
||||
]);
|
||||
|
||||
for (const asset of assetsToCopy) {
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 4.6 KiB |
@ -3,6 +3,13 @@
|
||||
|
||||
The Calendar view of Book notes will display each child note in a calendar that has a start date and optionally an end date, as an event.
|
||||
|
||||
The Calendar view has multiple display modes:
|
||||
|
||||
* Week view, where all the 7 days of the week (or 5 if the weekends are hidden) are displayed in columns. This mode allows entering and displaying time-specific events, not just all-day events.
|
||||
* Month view, where the entire month is displayed and all-day events can be inserted. Both time-specific events and all-day events are listed.
|
||||
* Year view, which displays the entire year for quick reference.
|
||||
* List view, which displays all the events of a given month in sequence.
|
||||
|
||||
Unlike other Book view types, the Calendar view also allows some kind of interaction, such as moving events around as well as creating new ones.
|
||||
|
||||
## Creating a calendar
|
||||
@ -23,7 +30,7 @@ Unlike other Book view types, the Calendar view also allows some kind of interac
|
||||
## Interacting with events
|
||||
|
||||
* Hovering the mouse over an event will display information about the note.
|
||||

|
||||

|
||||
* Left clicking the event will go to that note. Middle clicking will open the note in a new tab and right click will offer more options including opening the note in a new split or window.
|
||||
* Drag and drop an event on the calendar to move it to another day.
|
||||
* The length of an event can be changed by placing the mouse to the right edge of the event and dragging the mouse around.
|
||||
@ -49,8 +56,8 @@ For each note of the calendar, the following attributes can be used:
|
||||
| `#color` | Displays the event with a specified color (named such as `red`, `gray` or hex such as `#FF0000`). This will also change the color of the note in other places such as the note tree. |
|
||||
| `#calendar:color` | Similar to `#color`, but applies the color only for the event in the calendar and not for other places such as the note tree. |
|
||||
| `#iconClass` | If present, the icon of the note will be displayed to the left of the event title. |
|
||||
| `#calendar:title` | Changes the title of an event to point to an attribute of the note other than the title, either a label (e.g. `#assignee`) or a relation (e.g. `~for`). See _Advanced use-cases_ for more information. |
|
||||
| `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this: <br> <br> <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"` <br> <br>It can also be used with relations, case in which it will display the title of the target note: <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` |
|
||||
| `#calendar:title` | Changes the title of an event to point to an attribute of the note other than the title, can either a label or a relation (without the `#` or `~` symbol). See _Use-cases_ for more information. |
|
||||
| `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this: <br> <br> <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"` <br> <br>It can also be used with relations, case in which it will display the title of the target note: <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` |
|
||||
| `#calendar:startDate` | Allows using a different label to represent the start date, other than `startDate` (e.g. `expiryDate`). The label name **must not be** prefixed with `#`. If the label is not defined for a note, the default will be used instead. |
|
||||
| `#calendar:endDate` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the end date. |
|
||||
| `#calendar:startTime` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the start time. |
|
||||
@ -58,7 +65,7 @@ For each note of the calendar, the following attributes can be used:
|
||||
|
||||
## How the calendar works
|
||||
|
||||

|
||||

|
||||
|
||||
The calendar displays all the child notes of the book that have a `#startDate`. An `#endDate` can optionally be added.
|
||||
|
||||
@ -72,7 +79,7 @@ If editing the start date and end date from the note itself is desirable, the fo
|
||||
|
||||
This will result in:
|
||||
|
||||

|
||||

|
||||
|
||||
When not used in a Journal, the calendar is recursive. That is, it will look for events not just in its child notes but also in the children of these child notes.
|
||||
|
||||
@ -89,30 +96,28 @@ Based on the `#calendarRoot` (or `#workspaceCalendarRoot`) attribute, the calend
|
||||
* Clicking on the empty space on a date will automatically open that day's note or create it if it does not exist.
|
||||
* Direct children of a day note will be displayed on the calendar despite not having a `dateNote` attribute. Children of the child notes will not be displayed.
|
||||
|
||||

|
||||

|
||||
|
||||
### Using a different attribute as event title
|
||||
|
||||
By default, events are displayed on the calendar by their note title. However, it is possible to configure a different attribute to be displayed instead.
|
||||
|
||||
To do so, assign `#calendar:title` to the child note (not the calendar/book note), with the value being `#name` where `name` can be any label. The attribute can also come through inheritance such as a template attribute. If the note does not have the requested label, the title of the note will be used instead.
|
||||
To do so, assign `#calendar:title` to the child note (not the calendar/book note), with the value being `name` where `name` can be any label (make not to add the `#` prefix). The attribute can also come through inheritance such as a template attribute. If the note does not have the requested label, the title of the note will be used instead.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
<div class="joplin-table-wrapper"><table><thead><tr><th> </th><th> </th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre></td><td><img src="5_Calendar View_image.png"></td></tr></tbody></table></div>
|
||||
|
||||
### Using a relation attribute as event title
|
||||
|
||||
Similarly to using an attribute, use `#calendar:title` and set it to `~name` where `name` is the name of the relation to use.
|
||||
Similarly to using an attribute, use `#calendar:title` and set it to `name` where `name` is the name of the relation to use.
|
||||
|
||||
Moreover, if there are more relations of the same name, they will be displayed as multiple events coming from the same note.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
| `#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"` |  |
|
||||
|
||||
Note that it's even possible to have a `#calendar:title` on the target note (e.g. “John Smith”) which will try to render an attribute of it. Note that it's not possible to use a relation here as well for safety reasons (an accidental recursion of attributes could cause the application to loop infinitely).
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
| `#calendar:title="shortName" #shortName="John S."` |  |
|
10
package-lock.json
generated
@ -12,9 +12,6 @@
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@highlightjs/cdn-assets": "11.11.1",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.61",
|
||||
"@mermaid-js/layout-elk": "0.1.7",
|
||||
@ -116,6 +113,9 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@fullcalendar/list": "6.1.15",
|
||||
"@fullcalendar/multimonth": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
@ -2873,6 +2873,7 @@
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
|
||||
"integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
@ -2882,6 +2883,7 @@
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@ -2892,6 +2894,7 @@
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
|
||||
"integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
@ -2901,6 +2904,7 @@
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz",
|
||||
"integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
|
@ -72,11 +72,7 @@
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@highlightjs/cdn-assets": "11.11.1",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.61",
|
||||
"@mermaid-js/layout-elk": "0.1.7",
|
||||
"@mind-elixir/node-menu": "1.0.4",
|
||||
"@triliumnext/express-partial-content": "1.0.1",
|
||||
@ -173,6 +169,9 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@fullcalendar/list": "6.1.15",
|
||||
"@fullcalendar/multimonth": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
|
5
packages/turndown-plugin-gfm/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
dist
|
||||
lib
|
||||
node_modules
|
||||
npm-debug.log
|
||||
test/*browser.js
|
4
packages/turndown-plugin-gfm/.travis.yml
Normal file
@ -0,0 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- "6"
|
21
packages/turndown-plugin-gfm/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dom Christie
|
||||
|
||||
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.
|
64
packages/turndown-plugin-gfm/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# turndown-plugin-gfm
|
||||
|
||||
A [Turndown](https://github.com/domchristie/turndown) plugin which adds GitHub Flavored Markdown extensions.
|
||||
|
||||
This is a fork of the original [turndown-plugin-gfm](https://github.com/domchristie/turndown-plugin-gfm) for use with [Joplin](https://github.com/laurent22/joplin). The changes are:
|
||||
|
||||
- New: Always render tables even if they don't have a header.
|
||||
- New: Don't render the border of tables that contain other tables (frequent for websites that do the layout using tables). Only render the inner tables, if any, and if they also don't contain other tables.
|
||||
- New: Replace newlines (`\n`) with `<br>` inside table cells so that multi-line content is displayed correctly as Markdown.
|
||||
- New: Table cells are at least three characters long (padded with spaces) so that they render correctly in GFM-compliant renderers.
|
||||
- New: Handle colspan in TD tags
|
||||
- Fixed: Ensure there are no blank lines inside tables (due for example to an empty `<tr>` tag)
|
||||
- Fixed: Fixed importing tables that contain pipes.
|
||||
|
||||
## Installation
|
||||
|
||||
npm:
|
||||
|
||||
```
|
||||
npm install @joplin/turndown-plugin-gfm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
// For Node.js
|
||||
var TurndownService = require('@joplin/turndown')
|
||||
var turndownPluginGfm = require('@joplin/turndown-plugin-gfm')
|
||||
|
||||
var gfm = turndownPluginGfm.gfm
|
||||
var turndownService = new TurndownService()
|
||||
turndownService.use(gfm)
|
||||
var markdown = turndownService.turndown('<strike>Hello world!</strike>')
|
||||
```
|
||||
|
||||
turndown-plugin-gfm is a suite of plugins which can be applied individually. The available plugins are as follows:
|
||||
|
||||
- `strikethrough` (for converting `<strike>`, `<s>`, and `<del>` elements)
|
||||
- `tables`
|
||||
- `taskListItems`
|
||||
- `gfm` (which applies all of the above)
|
||||
|
||||
So for example, if you only wish to convert tables:
|
||||
|
||||
```js
|
||||
var tables = require('@joplin/turndown-plugin-gfm').tables
|
||||
var turndownService = new TurndownService()
|
||||
turndownService.use(tables)
|
||||
```
|
||||
|
||||
### Typescript
|
||||
|
||||
To use this in a typescript project, add this to a `declarations.d.ts` file, as described in https://www.npmjs.com/package/@joplin/turndown, and then add:
|
||||
|
||||
```ts
|
||||
declare module "@joplin/turndown-plugin-gfm" {
|
||||
export const gfm: any;
|
||||
// Add other named exports if necessary
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
turndown-plugin-gfm is copyright © 2017+ Dom Christie and released under the MIT license.
|
8
packages/turndown-plugin-gfm/build_for_test.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
ROOT_DIR="$SCRIPT_DIR/../.."
|
||||
|
||||
npm run build
|
||||
cd $ROOT_DIR/packages/app-cli && npm run test -- HtmlToMd
|
@ -0,0 +1,8 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
export default config({
|
||||
output: {
|
||||
format: 'cjs',
|
||||
file: 'lib/turndown-plugin-gfm.browser.cjs.js',
|
||||
},
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
export default config({
|
||||
output: {
|
||||
format: 'es',
|
||||
file: 'lib/turndown-plugin-gfm.browser.es.js',
|
||||
},
|
||||
});
|
8
packages/turndown-plugin-gfm/config/rollup.config.cjs.js
Normal file
@ -0,0 +1,8 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
export default config({
|
||||
output: {
|
||||
format: 'cjs',
|
||||
file: 'lib/turndown-plugin-gfm.cjs.js',
|
||||
},
|
||||
});
|
8
packages/turndown-plugin-gfm/config/rollup.config.es.js
Normal file
@ -0,0 +1,8 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
export default config({
|
||||
output: {
|
||||
format: 'es',
|
||||
file: 'lib/turndown-plugin-gfm.es.js',
|
||||
},
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
export default config({
|
||||
output: {
|
||||
format: 'iife',
|
||||
file: 'dist/turndown-plugin-gfm.js',
|
||||
},
|
||||
});
|
7
packages/turndown-plugin-gfm/config/rollup.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function(config) {
|
||||
return {
|
||||
name: 'turndownPluginGfm',
|
||||
input: 'src/gfm.js',
|
||||
output: config.output,
|
||||
};
|
||||
}
|
3595
packages/turndown-plugin-gfm/package-lock.json
generated
Normal file
45
packages/turndown-plugin-gfm/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@joplin/turndown-plugin-gfm",
|
||||
"description": "Turndown plugin to add GitHub Flavored Markdown extensions.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "1.0.61",
|
||||
"author": "Dom Christie",
|
||||
"main": "lib/turndown-plugin-gfm.cjs.js",
|
||||
"devDependencies": {
|
||||
"browserify": "14.5.0",
|
||||
"rollup": "0.50.1",
|
||||
"standard": "17.1.0",
|
||||
"turndown": "7.2.0",
|
||||
"turndown-attendant": "0.0.3"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"turndown",
|
||||
"turndown-plugin",
|
||||
"html-to-markdown",
|
||||
"html",
|
||||
"markdown",
|
||||
"github-flavored-markdown",
|
||||
"gfm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laurent22/joplin-turndown-plugin-gfm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build-all": "npm run build-cjs && npm run build-es && npm run build-iife",
|
||||
"build": "rollup -c config/rollup.config.cjs.js",
|
||||
"build-cjs": "rollup -c config/rollup.config.cjs.js && rollup -c config/rollup.config.browser.cjs.js",
|
||||
"build-es": "rollup -c config/rollup.config.es.js && rollup -c config/rollup.config.browser.es.js",
|
||||
"build-iife": "rollup -c config/rollup.config.iife.js",
|
||||
"build-test": "browserify test/turndown-plugin-gfm-test.js --outfile test/turndown-plugin-gfm-test.browser.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"gitHead": "05a29b450962bf05a8642bbd39446a1f679a96ba"
|
||||
}
|
3
packages/turndown-plugin-gfm/publish.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
npm version patch
|
||||
npm publish
|
15
packages/turndown-plugin-gfm/src/gfm.js
Normal file
@ -0,0 +1,15 @@
|
||||
import highlightedCodeBlock from './highlighted-code-block.js'
|
||||
import strikethrough from './strikethrough.js'
|
||||
import tables from './tables.js'
|
||||
import taskListItems from './task-list-items.js'
|
||||
|
||||
function gfm (turndownService) {
|
||||
turndownService.use([
|
||||
highlightedCodeBlock,
|
||||
strikethrough,
|
||||
tables,
|
||||
taskListItems
|
||||
])
|
||||
}
|
||||
|
||||
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems }
|
25
packages/turndown-plugin-gfm/src/highlighted-code-block.js
Normal file
@ -0,0 +1,25 @@
|
||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/
|
||||
|
||||
export default function highlightedCodeBlock (turndownService) {
|
||||
turndownService.addRule('highlightedCodeBlock', {
|
||||
filter: function (node) {
|
||||
var firstChild = node.firstChild
|
||||
return (
|
||||
node.nodeName === 'DIV' &&
|
||||
highlightRegExp.test(node.className) &&
|
||||
firstChild &&
|
||||
firstChild.nodeName === 'PRE'
|
||||
)
|
||||
},
|
||||
replacement: function (content, node, options) {
|
||||
var className = node.className || ''
|
||||
var language = (className.match(highlightRegExp) || [null, ''])[1]
|
||||
|
||||
return (
|
||||
'\n\n' + options.fence + language + '\n' +
|
||||
node.firstChild.textContent +
|
||||
'\n' + options.fence + '\n\n'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
8
packages/turndown-plugin-gfm/src/strikethrough.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default function strikethrough (turndownService) {
|
||||
turndownService.addRule('strikethrough', {
|
||||
filter: ['del', 's', 'strike'],
|
||||
replacement: function (content) {
|
||||
return '~~' + content + '~~'
|
||||
}
|
||||
})
|
||||
}
|
286
packages/turndown-plugin-gfm/src/tables.js
Normal file
@ -0,0 +1,286 @@
|
||||
var indexOf = Array.prototype.indexOf
|
||||
var every = Array.prototype.every
|
||||
var rules = {}
|
||||
var alignMap = { left: ':---', right: '---:', center: ':---:' };
|
||||
|
||||
let isCodeBlock_ = null;
|
||||
let options_ = null;
|
||||
|
||||
// We need to cache the result of tableShouldBeSkipped() as it is expensive.
|
||||
// Caching it means we went from about 9000 ms for rendering down to 90 ms.
|
||||
// Fixes https://github.com/laurent22/joplin/issues/6736
|
||||
const tableShouldBeSkippedCache_ = new WeakMap();
|
||||
|
||||
function getAlignment(node) {
|
||||
return node ? (node.getAttribute('align') || node.style.textAlign || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function getBorder(alignment) {
|
||||
return alignment ? alignMap[alignment] : '---';
|
||||
}
|
||||
|
||||
function getColumnAlignment(table, columnIndex) {
|
||||
var votes = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
center: 0,
|
||||
'': 0,
|
||||
};
|
||||
|
||||
var align = '';
|
||||
|
||||
for (var i = 0; i < table.rows.length; ++i) {
|
||||
var row = table.rows[i];
|
||||
if (columnIndex < row.childNodes.length) {
|
||||
var cellAlignment = getAlignment(row.childNodes[columnIndex]);
|
||||
++votes[cellAlignment];
|
||||
|
||||
if (votes[cellAlignment] > votes[align]) {
|
||||
align = cellAlignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return align;
|
||||
}
|
||||
|
||||
rules.tableCell = {
|
||||
filter: ['th', 'td'],
|
||||
replacement: function (content, node) {
|
||||
if (tableShouldBeSkipped(nodeParentTable(node))) return content;
|
||||
return cell(content, node)
|
||||
}
|
||||
}
|
||||
|
||||
rules.tableRow = {
|
||||
filter: 'tr',
|
||||
replacement: function (content, node) {
|
||||
const parentTable = nodeParentTable(node);
|
||||
if (tableShouldBeSkipped(parentTable)) return content;
|
||||
|
||||
var borderCells = ''
|
||||
|
||||
if (isHeadingRow(node)) {
|
||||
const colCount = tableColCount(parentTable);
|
||||
for (var i = 0; i < colCount; i++) {
|
||||
const childNode = i < node.childNodes.length ? node.childNodes[i] : null;
|
||||
var border = getBorder(getColumnAlignment(parentTable, i));
|
||||
borderCells += cell(border, childNode, i);
|
||||
}
|
||||
}
|
||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
||||
}
|
||||
}
|
||||
|
||||
rules.table = {
|
||||
filter: function (node, options) {
|
||||
return node.nodeName === 'TABLE';
|
||||
},
|
||||
|
||||
replacement: function (content, node) {
|
||||
// Only convert tables that can result in valid Markdown
|
||||
// Other tables are kept as HTML using `keep` (see below).
|
||||
if (tableShouldBeHtml(node, options_)) {
|
||||
return node.outerHTML;
|
||||
} else {
|
||||
if (tableShouldBeSkipped(node)) return content;
|
||||
|
||||
// Ensure there are no blank lines
|
||||
content = content.replace(/\n+/g, '\n')
|
||||
|
||||
// If table has no heading, add an empty one so as to get a valid Markdown table
|
||||
var secondLine = content.trim().split('\n');
|
||||
if (secondLine.length >= 2) secondLine = secondLine[1]
|
||||
var secondLineIsDivider = /\| :?---/.test(secondLine);
|
||||
|
||||
var columnCount = tableColCount(node);
|
||||
var emptyHeader = ''
|
||||
if (columnCount && !secondLineIsDivider) {
|
||||
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|'
|
||||
for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
|
||||
emptyHeader += ' ' + getBorder(getColumnAlignment(node, columnIndex)) + ' |';
|
||||
}
|
||||
}
|
||||
|
||||
const captionContent = node.caption ? node.caption.textContent || '' : '';
|
||||
const caption = captionContent ? `${captionContent}\n\n` : '';
|
||||
const tableContent = `${emptyHeader}${content}`.trimStart();
|
||||
return `\n\n${caption}${tableContent}\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rules.tableCaption = {
|
||||
filter: ['caption'],
|
||||
replacement: () => '',
|
||||
};
|
||||
|
||||
rules.tableColgroup = {
|
||||
filter: ['colgroup', 'col'],
|
||||
replacement: () => '',
|
||||
};
|
||||
|
||||
rules.tableSection = {
|
||||
filter: ['thead', 'tbody', 'tfoot'],
|
||||
replacement: function (content) {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// A tr is a heading row if:
|
||||
// - the parent is a THEAD
|
||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
||||
// following a blank THEAD)
|
||||
// - and every cell is a TH
|
||||
function isHeadingRow (tr) {
|
||||
var parentNode = tr.parentNode
|
||||
return (
|
||||
parentNode.nodeName === 'THEAD' ||
|
||||
(
|
||||
parentNode.firstChild === tr &&
|
||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function isFirstTbody (element) {
|
||||
var previousSibling = element.previousSibling
|
||||
return (
|
||||
element.nodeName === 'TBODY' && (
|
||||
!previousSibling ||
|
||||
(
|
||||
previousSibling.nodeName === 'THEAD' &&
|
||||
/^\s*$/i.test(previousSibling.textContent)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function cell (content, node = null, index = null) {
|
||||
if (index === null) index = indexOf.call(node.parentNode.childNodes, node)
|
||||
var prefix = ' '
|
||||
if (index === 0) prefix = '| '
|
||||
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
|
||||
filteredContent = filteredContent.replace(/\|+/g, '\\|')
|
||||
while (filteredContent.length < 3) filteredContent += ' ';
|
||||
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
|
||||
return prefix + filteredContent + ' |'
|
||||
}
|
||||
|
||||
function nodeContainsTable(node) {
|
||||
if (!node.childNodes) return false;
|
||||
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const child = node.childNodes[i];
|
||||
if (child.nodeName === 'TABLE') return true;
|
||||
if (nodeContainsTable(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeContains = (node, types) => {
|
||||
if (!node.childNodes) return false;
|
||||
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const child = node.childNodes[i];
|
||||
if (types === 'code' && isCodeBlock_ && isCodeBlock_(child)) return true;
|
||||
if (types.includes(child.nodeName)) return true;
|
||||
if (nodeContains(child, types)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableShouldBeHtml = (tableNode, options) => {
|
||||
const possibleTags = [
|
||||
'UL',
|
||||
'OL',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'HR',
|
||||
'BLOCKQUOTE',
|
||||
'PRE'
|
||||
];
|
||||
|
||||
// In general we should leave as HTML tables that include other tables. The
|
||||
// exception is with the Web Clipper when we import a web page with a layout
|
||||
// that's made of HTML tables. In that case we have this logic of removing the
|
||||
// outer table and keeping only the inner ones. For the Rich Text editor
|
||||
// however we always want to keep nested tables.
|
||||
if (options.preserveNestedTables) possibleTags.push('TABLE');
|
||||
|
||||
return nodeContains(tableNode, 'code') ||
|
||||
nodeContains(tableNode, possibleTags);
|
||||
}
|
||||
|
||||
// Various conditions under which a table should be skipped - i.e. each cell
|
||||
// will be rendered one after the other as if they were paragraphs.
|
||||
function tableShouldBeSkipped(tableNode) {
|
||||
const cached = tableShouldBeSkippedCache_.get(tableNode);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const result = tableShouldBeSkipped_(tableNode);
|
||||
|
||||
tableShouldBeSkippedCache_.set(tableNode, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function tableShouldBeSkipped_(tableNode) {
|
||||
if (!tableNode) return true;
|
||||
if (!tableNode.rows) return true;
|
||||
if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell
|
||||
if (nodeContainsTable(tableNode)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function nodeParentDiv(node) {
|
||||
let parent = node.parentNode;
|
||||
while (parent.nodeName !== 'DIV') {
|
||||
parent = parent.parentNode;
|
||||
if (!parent) return null;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function nodeParentTable(node) {
|
||||
let parent = node.parentNode;
|
||||
while (parent.nodeName !== 'TABLE') {
|
||||
parent = parent.parentNode;
|
||||
if (!parent) return null;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function handleColSpan(content, node, emptyChar) {
|
||||
const colspan = node.getAttribute('colspan') || 1;
|
||||
for (let i = 1; i < colspan; i++) {
|
||||
content += ' | ' + emptyChar.repeat(3);
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function tableColCount(node) {
|
||||
let maxColCount = 0;
|
||||
for (let i = 0; i < node.rows.length; i++) {
|
||||
const row = node.rows[i]
|
||||
const colCount = row.childNodes.length
|
||||
if (colCount > maxColCount) maxColCount = colCount
|
||||
}
|
||||
return maxColCount
|
||||
}
|
||||
|
||||
export default function tables (turndownService) {
|
||||
isCodeBlock_ = turndownService.isCodeBlock;
|
||||
options_ = turndownService.options;
|
||||
|
||||
turndownService.keep(function (node) {
|
||||
if (node.nodeName === 'TABLE' && tableShouldBeHtml(node, turndownService.options)) return true;
|
||||
return false;
|
||||
});
|
||||
for (var key in rules) turndownService.addRule(key, rules[key])
|
||||
}
|
10
packages/turndown-plugin-gfm/src/task-list-items.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default function taskListItems (turndownService) {
|
||||
turndownService.addRule('taskListItems', {
|
||||
filter: function (node) {
|
||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
||||
},
|
||||
replacement: function (content, node) {
|
||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
||||
}
|
||||
})
|
||||
}
|
323
packages/turndown-plugin-gfm/test/index.html
Normal file
@ -0,0 +1,323 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>turndown test runner</title>
|
||||
<link rel="stylesheet" href="../node_modules/turndown-attendant/dist/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- TEST CASES -->
|
||||
|
||||
<div class="case" data-name="strike">
|
||||
<div class="input"><strike>Lorem ipsum</strike></div>
|
||||
<pre class="expected">~Lorem ipsum~</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="s">
|
||||
<div class="input"><s>Lorem ipsum</s></div>
|
||||
<pre class="expected">~Lorem ipsum~</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="del">
|
||||
<div class="input"><del>Lorem ipsum</del></div>
|
||||
<pre class="expected">~Lorem ipsum~</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="unchecked inputs">
|
||||
<div class="input"><ul><li><input type=checkbox>Check Me!</li></ul></div>
|
||||
<pre class="expected">* [ ] Check Me!</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="checked inputs">
|
||||
<div class="input"><ul><li><input type=checkbox checked>Checked!</li></ul></div>
|
||||
<pre class="expected">* [x] Checked!</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="basic table">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column 1</th>
|
||||
<th>Column 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Row 1, Column 1</td>
|
||||
<td>Row 1, Column 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 2, Column 1</td>
|
||||
<td>Row 2, Column 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Column 1 | Column 2 |
|
||||
| --- | --- |
|
||||
| Row 1, Column 1 | Row 1, Column 2 |
|
||||
| Row 2, Column 1 | Row 2, Column 2 |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="cell alignment">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left">Column 1</th>
|
||||
<th align="center">Column 2</th>
|
||||
<th align="right">Column 3</th>
|
||||
<th align="foo">Column 4</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Row 1, Column 1</td>
|
||||
<td>Row 1, Column 2</td>
|
||||
<td>Row 1, Column 3</td>
|
||||
<td>Row 1, Column 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 2, Column 1</td>
|
||||
<td>Row 2, Column 2</td>
|
||||
<td>Row 2, Column 3</td>
|
||||
<td>Row 2, Column 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
|
||||
| :-- | :-: | --: | --- |
|
||||
| Row 1, Column 1 | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
|
||||
| Row 2, Column 1 | Row 2, Column 2 | Row 2, Column 3 | Row 2, Column 4 |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="empty cells">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left">Column 1</th>
|
||||
<th align="center">Column 2</th>
|
||||
<th align="right">Column 3</th>
|
||||
<th align="foo">Column 4</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Row 1, Column 2</td>
|
||||
<td>Row 1, Column 3</td>
|
||||
<td>Row 1, Column 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 2, Column 1</td>
|
||||
<td></td>
|
||||
<td>Row 2, Column 3</td>
|
||||
<td>Row 2, Column 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 3, Column 1</td>
|
||||
<td>Row 3, Column 2</td>
|
||||
<td></td>
|
||||
<td>Row 3, Column 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 4, Column 1</td>
|
||||
<td>Row 4, Column 2</td>
|
||||
<td>Row 4, Column 3</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>Row 5, Column 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
|
||||
| :-- | :-: | --: | --- |
|
||||
| | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
|
||||
| Row 2, Column 1 | | Row 2, Column 3 | Row 2, Column 4 |
|
||||
| Row 3, Column 1 | Row 3, Column 2 | | Row 3, Column 4 |
|
||||
| Row 4, Column 1 | Row 4, Column 2 | Row 4, Column 3 | |
|
||||
| | | | Row 5, Column 4 |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="empty rows">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead>
|
||||
<td>Heading 1</td>
|
||||
<td>Heading 2</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Row 1</td>
|
||||
<td>Row 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 3</td>
|
||||
<td>Row 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading 1 | Heading 2 |
|
||||
| --- | --- |
|
||||
| Row 1 | Row 1 |
|
||||
| Row 3 | Row 3 |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="th in first row">
|
||||
<div class="input">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Heading</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Content</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading |
|
||||
| --- |
|
||||
| Content |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="th first row in tbody">
|
||||
<div class="input">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Heading</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading |
|
||||
| --- |
|
||||
| Content |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="table with two tbodies">
|
||||
<div class="input">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Heading</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Heading</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading |
|
||||
| --- |
|
||||
| Content |
|
||||
| Heading |
|
||||
| Content |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="heading cells in both thead and tbody">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead><tr><th>Heading</th></tr></thead>
|
||||
<tbody><tr><th>Cell</th></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading |
|
||||
| --- |
|
||||
| Cell |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="empty head">
|
||||
<div class="input">
|
||||
<table>
|
||||
<thead><tr><th></th></tr></thead>
|
||||
<tbody><tr><th>Heading</th></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| Heading |
|
||||
| --- |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="non-definitive heading row (converted but with empty header)">
|
||||
<div class="input">
|
||||
<table>
|
||||
<tr><td>Row 1 Cell 1</td><td>Row 1 Cell 2</td></tr>
|
||||
<tr><td>Row 2 Cell 1</td><td>Row 2 Cell 2</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| | |
|
||||
| --- | --- |
|
||||
| Row 1 Cell 1 | Row 1 Cell 2 |
|
||||
| Row 2 Cell 1 | Row 2 Cell 2 |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="non-definitive heading row with th (converted but with empty header)">
|
||||
<div class="input">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Heading</th>
|
||||
<td>Not a heading</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Heading</td>
|
||||
<td>Not a heading</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<pre class="expected">| | |
|
||||
| --- | --- |
|
||||
| Heading | Not a heading |
|
||||
| Heading | Not a heading |</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="highlighted code block with html">
|
||||
<div class="input">
|
||||
<div class="highlight highlight-text-html-basic">
|
||||
<pre><<span class="pl-ent">p</span>>Hello world</<span class="pl-ent">p</span>></pre>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="expected">```html
|
||||
<p>Hello world</p>
|
||||
```</pre>
|
||||
</div>
|
||||
|
||||
<div class="case" data-name="highlighted code block with js">
|
||||
<div class="input">
|
||||
<div class="highlight highlight-source-js">
|
||||
<pre>;(<span class="pl-k">function</span> () {})()</pre>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="expected">```js
|
||||
;(function () {})()
|
||||
```</pre>
|
||||
</div>
|
||||
|
||||
<!-- /TEST CASES -->
|
||||
|
||||
<script src="turndown-plugin-gfm-test.browser.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,13 @@
|
||||
const Attendant = require('turndown-attendant');
|
||||
const TurndownService = require('turndown');
|
||||
const gfm = require('../lib/turndown-plugin-gfm.cjs').gfm;
|
||||
|
||||
const attendant = new Attendant({
|
||||
file: `${__dirname}/index.html`,
|
||||
TurndownService: TurndownService,
|
||||
beforeEach: function(turndownService) {
|
||||
turndownService.use(gfm);
|
||||
},
|
||||
});
|
||||
|
||||
attendant.run();
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 4.6 KiB |
@ -18,6 +18,16 @@
|
||||
</p>
|
||||
<p>The Calendar view of Book notes will display each child note in a calendar
|
||||
that has a start date and optionally an end date, as an event.</p>
|
||||
<p>The Calendar view has multiple display modes:</p>
|
||||
<ul>
|
||||
<li>Week view, where all the 7 days of the week (or 5 if the weekends are
|
||||
hidden) are displayed in columns. This mode allows entering and displaying
|
||||
time-specific events, not just all-day events.</li>
|
||||
<li>Month view, where the entire month is displayed and all-day events can
|
||||
be inserted. Both time-specific events and all-day events are listed.</li>
|
||||
<li>Year view, which displays the entire year for quick reference.</li>
|
||||
<li>List view, which displays all the events of a given month in sequence.</li>
|
||||
</ul>
|
||||
<p>Unlike other Book view types, the Calendar view also allows some kind
|
||||
of interaction, such as moving events around as well as creating new ones.</p>
|
||||
<h2>Creating a calendar</h2>
|
||||
@ -72,7 +82,7 @@
|
||||
<ul>
|
||||
<li>Hovering the mouse over an event will display information about the note.
|
||||
<br>
|
||||
<img src="9_Calendar View_image.png">
|
||||
<img src="7_Calendar View_image.png">
|
||||
</li>
|
||||
<li>Left clicking the event will go to that note. Middle clicking will open
|
||||
the note in a new tab and right click will offer more options including
|
||||
@ -83,53 +93,55 @@
|
||||
</ul>
|
||||
<h2>Configuring the calendar</h2>
|
||||
<p>The following attributes can be added to the book type:</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#calendar:hideWeekends</code>
|
||||
</td>
|
||||
<td>When present (regardless of value), it will hide Saturday and Sundays
|
||||
from the calendar.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#calendar:weekNumbers</code>
|
||||
</td>
|
||||
<td>When present (regardless of value), it will show the number of the week
|
||||
on the calendar.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#calendar:view</code>
|
||||
</td>
|
||||
<td>
|
||||
<p>Which view to display in the calendar:</p>
|
||||
<ul>
|
||||
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
</ul>
|
||||
<p>Any other value will be dismissed and the default view (month) will be
|
||||
used instead.</p>
|
||||
<p>The value of this label is automatically updated when changing the view
|
||||
using the UI buttons.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>~child:template</code>
|
||||
</td>
|
||||
<td>Defines the template for newly created notes in the calendar (via dragging
|
||||
or clicking).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<div>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#calendar:hideWeekends</code>
|
||||
</td>
|
||||
<td>When present (regardless of value), it will hide Saturday and Sundays
|
||||
from the calendar.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#calendar:weekNumbers</code>
|
||||
</td>
|
||||
<td>When present (regardless of value), it will show the number of the week
|
||||
on the calendar.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#calendar:view</code>
|
||||
</td>
|
||||
<td>
|
||||
<p>Which view to display in the calendar:</p>
|
||||
<ul>
|
||||
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
</ul>
|
||||
<p>Any other value will be dismissed and the default view (month) will be
|
||||
used instead.</p>
|
||||
<p>The value of this label is automatically updated when changing the view
|
||||
using the UI buttons.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>~child:template</code>
|
||||
</td>
|
||||
<td>Defines the template for newly created notes in the calendar (via dragging
|
||||
or clicking).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</div>
|
||||
<p>In addition, the first day of the week can be either Sunday or Monday
|
||||
and can be adjusted from the application settings.</p>
|
||||
<h2>Configuring the calendar events</h2>
|
||||
@ -194,22 +206,22 @@
|
||||
<td><code>#calendar:title</code>
|
||||
</td>
|
||||
<td>Changes the title of an event to point to an attribute of the note other
|
||||
than the title, either a label (e.g. <code>#assignee</code>) or a relation
|
||||
(e.g. <code>~for</code>). See <em>Advanced use-cases</em> for more information.</td>
|
||||
than the title, can either a label or a relation (without the <code>#</code> or <code>~</code> symbol).
|
||||
See <em>Use-cases</em> for more information.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#calendar:displayedAttributes</code>
|
||||
</td>
|
||||
<td>Allows displaying the value of one or more attributes in the calendar
|
||||
like this:
|
||||
like this:
|
||||
<br>
|
||||
<br>
|
||||
<img src="11_Calendar View_image.png">
|
||||
<img src="9_Calendar View_image.png">
|
||||
<br>
|
||||
<br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>
|
||||
<br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>
|
||||
<br>
|
||||
<br>It can also be used with relations, case in which it will display the
|
||||
title of the target note:
|
||||
title of the target note:
|
||||
<br>
|
||||
<br><code>~assignee=@My assignee #calendar:displayedAttributes="assignee"</code>
|
||||
</td>
|
||||
@ -244,7 +256,7 @@
|
||||
</figure>
|
||||
<h2>How the calendar works</h2>
|
||||
<p>
|
||||
<img src="14_Calendar View_image.png">
|
||||
<img src="11_Calendar View_image.png">
|
||||
</p>
|
||||
<p>The calendar displays all the child notes of the book that have a <code>#startDate</code>.
|
||||
An <code>#endDate</code> can optionally be added.</p>
|
||||
@ -254,7 +266,7 @@
|
||||
#hidePromotedAttributes </code></pre>
|
||||
<p>This will result in:</p>
|
||||
<p>
|
||||
<img src="12_Calendar View_image.png">
|
||||
<img src="10_Calendar View_image.png">
|
||||
</p>
|
||||
<p>When not used in a Journal, the calendar is recursive. That is, it will
|
||||
look for events not just in its child notes but also in the children of
|
||||
@ -279,18 +291,19 @@
|
||||
will not be displayed.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<img src="10_Calendar View_image.png">
|
||||
<img src="8_Calendar View_image.png">
|
||||
</p>
|
||||
<h3>Using a different attribute as event title</h3>
|
||||
<p>By default, events are displayed on the calendar by their note title.
|
||||
However, it is possible to configure a different attribute to be displayed
|
||||
instead.</p>
|
||||
<p>To do so, assign <code>#calendar:title</code> to the child note (not the
|
||||
calendar/book note), with the value being <code>#name</code> where <code>name</code> can
|
||||
be any label. The attribute can also come through inheritance such as a
|
||||
template attribute. If the note does not have the requested label, the
|
||||
title of the note will be used instead.</p>
|
||||
<figure class="table">
|
||||
calendar/book note), with the value being <code>name</code> where <code>name</code> can
|
||||
be any label (make not to add the <code>#</code> prefix). The attribute can
|
||||
also come through inheritance such as a template attribute. If the note
|
||||
does not have the requested label, the title of the note will be used instead.</p>
|
||||
<figure
|
||||
class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -300,67 +313,64 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre>
|
||||
</td>
|
||||
<td>
|
||||
<img src="5_Calendar View_image.png">
|
||||
</td>
|
||||
<td>
|
||||
<img src="7_Calendar View_image.png">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h3>Using a relation attribute as event title</h3>
|
||||
<p>Similarly to using an attribute, use <code>#calendar:title</code> and set
|
||||
it to <code>~name</code> where <code>name</code> is the name of the relation
|
||||
to use.</p>
|
||||
<p>Moreover, if there are more relations of the same name, they will be displayed
|
||||
as multiple events coming from the same note.</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="6_Calendar View_image.png">
|
||||
</td>
|
||||
<td>
|
||||
<img src="8_Calendar View_image.png">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<p>Note that it's even possible to have a <code>#calendar:title</code> on the
|
||||
target note (e.g. “John Smith”) which will try to render an attribute of
|
||||
it. Note that it's not possible to use a relation here as well for safety
|
||||
reasons (an accidental recursion of attributes could cause the application
|
||||
to loop infinitely).</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="13_Calendar View_image.png">
|
||||
</td>
|
||||
<td>
|
||||
<img src="1_Calendar View_image.png">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</figure>
|
||||
<h3>Using a relation attribute as event title</h3>
|
||||
<p>Similarly to using an attribute, use <code>#calendar:title</code> and set
|
||||
it to <code>name</code> where <code>name</code> is the name of the relation
|
||||
to use.</p>
|
||||
<p>Moreover, if there are more relations of the same name, they will be displayed
|
||||
as multiple events coming from the same note.</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"</code>
|
||||
</td>
|
||||
<td>
|
||||
<img src="6_Calendar View_image.png">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<p>Note that it's even possible to have a <code>#calendar:title</code> on the
|
||||
target note (e.g. “John Smith”) which will try to render an attribute of
|
||||
it. Note that it's not possible to use a relation here as well for safety
|
||||
reasons (an accidental recursion of attributes could cause the application
|
||||
to loop infinitely).</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#calendar:title="shortName" #shortName="John S."</code>
|
||||
</td>
|
||||
<td>
|
||||
<img src="1_Calendar View_image.png">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -13,62 +13,59 @@
|
||||
<h1 data-trilium-h1>Admonitions</h1>
|
||||
|
||||
<div class="ck-content">
|
||||
<div>
|
||||
<div>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:959/547;" src="1_Admonitions_image.png" width="959"
|
||||
height="547">
|
||||
</figure>
|
||||
<p>Admonitions are a way to highlight information to the reader. Other names
|
||||
for it include <em>call-outs</em> and <em>info/warning/alert boxes</em>.</p>
|
||||
<h2>Inserting a new admonition</h2>
|
||||
<h3>From the UI</h3>
|
||||
<p>In the Formatting toolbar:</p>
|
||||
<p>
|
||||
<img src="Admonitions_image.png" width="202" height="194">
|
||||
</p>
|
||||
<h3>Via the keyboard</h3>
|
||||
<p>It's possible to insert an admonition simply by typing:</p>
|
||||
<ul>
|
||||
<li><code>!!! note</code>
|
||||
</li>
|
||||
<li><code>!!! tip</code>
|
||||
</li>
|
||||
<li><code>!!! important</code>
|
||||
</li>
|
||||
<li><code>!!! caution</code>
|
||||
</li>
|
||||
<li><code>!!! warning</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p>In addition to that, it's also possible to type <code>!!! </code> followed
|
||||
by any text, case in which a default admonition type will appear (note)
|
||||
with the entered text inside it.</p>
|
||||
<h2>Interaction</h2>
|
||||
<p>By design, admonitions act very similar to block quotes.</p>
|
||||
<ul>
|
||||
<li>Selecting a text and pressing the admonition button will turn that text
|
||||
into an admonition.</li>
|
||||
<li>If selecting multiple admonitions, pressing the admonition button will
|
||||
automatically merge them into one.</li>
|
||||
</ul>
|
||||
<p>Inside an admonition:</p>
|
||||
<ul>
|
||||
<li>Pressing <kbd>Backspace</kbd> while the admonition is empty will remove
|
||||
it.</li>
|
||||
<li>Pressing <kbd>Enter</kbd> will start a new paragraph. Pressing it twice
|
||||
will exit out of the admonition.</li>
|
||||
<li>Headings and other block content including tables can be inserted inside
|
||||
the admonition.</li>
|
||||
</ul>
|
||||
<h2>Types of admonitions</h2>
|
||||
<p>There are currently five types of admonitions: <em>Note</em>, <em>Tip</em>, <em>Important</em>, <em>Caution</em>, <em>Warning</em>.</p>
|
||||
<p>These types were inspired by GitHub's support for this feature and there
|
||||
are currently no plans for adjusting it or allowing the user to customize
|
||||
them.</p>
|
||||
<h2>Markdown support</h2>
|
||||
<p>The Markdown syntax for admonitions as supported by Trilium is the one
|
||||
that GitHub uses, which is as follows:</p><pre><code class="language-text-x-markdown">> [!NOTE]
|
||||
<p>
|
||||
<img src="1_Admonitions_image.png">
|
||||
</p>
|
||||
<p>Admonitions are a way to highlight information to the reader. Other names
|
||||
for it include <em>call-outs</em> and <em>info/warning/alert boxes</em>.</p>
|
||||
<h2>Inserting a new admonition</h2>
|
||||
<h3>From the UI</h3>
|
||||
<p>In the Formatting toolbar:</p>
|
||||
<p>
|
||||
<img src="Admonitions_image.png">
|
||||
</p>
|
||||
<h3>Via the keyboard</h3>
|
||||
<p>It's possible to insert an admonition simply by typing:</p>
|
||||
<ul>
|
||||
<li><code>!!! note</code>
|
||||
</li>
|
||||
<li><code>!!! tip</code>
|
||||
</li>
|
||||
<li><code>!!! important</code>
|
||||
</li>
|
||||
<li><code>!!! caution</code>
|
||||
</li>
|
||||
<li><code>!!! warning</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p>In addition to that, it's also possible to type <code>!!!</code> followed
|
||||
by any text, case in which a default admonition type will appear (note)
|
||||
with the entered text inside it.</p>
|
||||
<h2>Interaction</h2>
|
||||
<p>By design, admonitions act very similar to block quotes.</p>
|
||||
<ul>
|
||||
<li>Selecting a text and pressing the admonition button will turn that text
|
||||
into an admonition.</li>
|
||||
<li>If selecting multiple admonitions, pressing the admonition button will
|
||||
automatically merge them into one.</li>
|
||||
</ul>
|
||||
<p>Inside an admonition:</p>
|
||||
<ul>
|
||||
<li>Pressing <kbd>Backspace</kbd> while the admonition is empty will remove
|
||||
it.</li>
|
||||
<li>Pressing <kbd>Enter</kbd> will start a new paragraph. Pressing it twice
|
||||
will exit out of the admonition.</li>
|
||||
<li>Headings and other block content including tables can be inserted inside
|
||||
the admonition.</li>
|
||||
</ul>
|
||||
<h2>Types of admonitions</h2>
|
||||
<p>There are currently five types of admonitions: <em>Note</em>, <em>Tip</em>, <em>Important</em>, <em>Caution</em>, <em>Warning</em>.</p>
|
||||
<p>These types were inspired by GitHub's support for this feature and there
|
||||
are currently no plans for adjusting it or allowing the user to customize
|
||||
them.</p>
|
||||
<h2>Markdown support</h2>
|
||||
<p>The Markdown syntax for admonitions as supported by Trilium is the one
|
||||
that GitHub uses, which is as follows:</p><pre><code class="language-text-x-gfm">> [!NOTE]
|
||||
> This is a note.
|
||||
|
||||
> [!TIP]
|
||||
@ -79,10 +76,8 @@
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a caution.</code></pre>
|
||||
<p>There are currently no plans of supporting alternative admonition syntaxes
|
||||
such as <code>!!! note</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>There are currently no plans of supporting alternative admonition syntaxes
|
||||
such as <code>!!! note</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
37
src/public/app/widgets/floating_buttons/help_button.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { byBookType, byNoteType } from "./help_button.js";
|
||||
import fs from "fs";
|
||||
import type { NoteMetaFile } from "../../../../services/meta/note_meta.js";
|
||||
import type NoteMeta from "../../../../services/meta/note_meta.js";
|
||||
|
||||
describe("Help button", () => {
|
||||
it("All help notes are accessible", () => {
|
||||
function getNoteIds(item: NoteMeta | NoteMetaFile): string[] {
|
||||
const items = [];
|
||||
|
||||
if ("noteId" in item && item.noteId) {
|
||||
items.push(item.noteId);
|
||||
}
|
||||
|
||||
const children = "files" in item ? item.files : item.children;
|
||||
for (const child of children ?? []) {
|
||||
items.push(getNoteIds(child));
|
||||
}
|
||||
return items.flat();
|
||||
}
|
||||
|
||||
const allHelpNotes = [
|
||||
...Object.values(byNoteType),
|
||||
...Object.values(byBookType)
|
||||
].filter((noteId) => noteId) as string[];
|
||||
|
||||
const meta: NoteMetaFile = JSON.parse(fs.readFileSync("src/public/app/doc_notes/en/User Guide/!!!meta.json", "utf-8"));
|
||||
const allNoteIds = new Set(getNoteIds(meta));
|
||||
|
||||
for (const helpNote of allHelpNotes) {
|
||||
if (!allNoteIds.has(helpNote)) {
|
||||
expect.fail(`Help note with ID ${helpNote} does not exist in the in-app help.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
@ -12,13 +12,13 @@ const TPL = `
|
||||
</button>
|
||||
`;
|
||||
|
||||
const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
canvas: null,
|
||||
code: null,
|
||||
contentWidget: null,
|
||||
doc: null,
|
||||
file: null,
|
||||
geoMap: "foPEtsL51pD2",
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
image: null,
|
||||
launcher: null,
|
||||
mermaid: null,
|
||||
@ -31,10 +31,10 @@ const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
webView: null
|
||||
};
|
||||
|
||||
const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: null,
|
||||
grid: null,
|
||||
calendar: "fDGg7QcJg3Xm"
|
||||
calendar: "xWbu3jpNWapp"
|
||||
};
|
||||
|
||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
|
@ -22,7 +22,7 @@ const TPL = `
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu {
|
||||
.map-container .node-menu {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
@ -38,28 +38,28 @@ const TPL = `
|
||||
transition: .3s all
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu.close {
|
||||
.map-container .node-menu.close {
|
||||
height: 29px;
|
||||
width: 46px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .button-container {
|
||||
.map-container .node-menu .button-container {
|
||||
padding: 3px 0;
|
||||
direction: rtl
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu #nm-tag {
|
||||
.map-container .node-menu #nm-tag {
|
||||
margin-top: 20px
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .nm-fontsize-container {
|
||||
.map-container .node-menu .nm-fontsize-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .nm-fontsize-container div {
|
||||
.map-container .node-menu .nm-fontsize-container div {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
@ -71,12 +71,12 @@ const TPL = `
|
||||
border-radius: 100%
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .nm-fontcolor-container {
|
||||
.map-container .node-menu .nm-fontcolor-container {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu input,
|
||||
.mind-elixir .node-menu textarea {
|
||||
.map-container .node-menu input,
|
||||
.map-container .node-menu textarea {
|
||||
background: var(--input-background-color);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
@ -87,17 +87,17 @@ const TPL = `
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu textarea {
|
||||
.map-container .node-menu textarea {
|
||||
resize: none
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .split6 {
|
||||
.map-container .node-menu .split6 {
|
||||
display: inline-block;
|
||||
width: 16.66%;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .palette {
|
||||
.map-container .node-menu .palette {
|
||||
border-radius: 100%;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
@ -105,35 +105,35 @@ const TPL = `
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .nmenu-selected,
|
||||
.mind-elixir .node-menu .palette:hover {
|
||||
.map-container .node-menu .nmenu-selected,
|
||||
.map-container .node-menu .palette:hover {
|
||||
box-shadow: tomato 0 0 0 2px;
|
||||
background-color: #c7e9fa
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .size-selected {
|
||||
.map-container .node-menu .size-selected {
|
||||
background-color: tomato !important;
|
||||
border-color: tomato;
|
||||
fill: #fff;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .size-selected svg {
|
||||
.map-container .node-menu .size-selected svg {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .bof {
|
||||
.map-container .node-menu .bof {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .bof span {
|
||||
.map-container .node-menu .bof span {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 5px
|
||||
}
|
||||
|
||||
.mind-elixir .node-menu .bof .selected {
|
||||
.map-container .node-menu .bof .selected {
|
||||
background-color: tomato;
|
||||
color: #fff
|
||||
}
|
||||
|
@ -127,13 +127,24 @@
|
||||
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
|
||||
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
|
||||
|
||||
--launcher-pane-background-color: #1a1a1a;
|
||||
--launcher-pane-horizontal-background-color: #282828;
|
||||
--launcher-pane-horizontal-border-color: rgb(22, 22, 22);
|
||||
--launcher-pane-text-color: #909090;
|
||||
--launcher-pane-button-hover-color: #ffffff;
|
||||
--launcher-pane-button-hover-background: #ffffff1c;
|
||||
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
|
||||
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
|
||||
--launcher-pane-background-color: unset;
|
||||
--launcher-pane-text-color: unset;
|
||||
|
||||
--launcher-pane-vert-background-color: #1a1a1a;
|
||||
--launcher-pane-vert-text-color: #909090;
|
||||
--launcher-pane-vert-button-hover-color: #ffffff;
|
||||
--launcher-pane-vert-button-hover-background: #ffffff1c;
|
||||
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
|
||||
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--launcher-pane-horiz-border-color: rgb(22, 22, 22);
|
||||
--launcher-pane-horiz-background-color: #282828;
|
||||
--launcher-pane-horiz-text-color: #909090;
|
||||
--launcher-pane-horiz-button-hover-color: #ffffff;
|
||||
--launcher-pane-horiz-button-hover-background: #ffffff1c;
|
||||
--launcher-pane-horiz-button-hover-shadow: unset;
|
||||
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--protected-session-active-icon-color: #8edd8e;
|
||||
--sync-status-error-pulse-color: #f47871;
|
||||
|
@ -121,13 +121,23 @@
|
||||
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
|
||||
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
|
||||
|
||||
--launcher-pane-background-color: #e8e8e8;
|
||||
--launcher-pane-horizontal-background-color: #fafafa;
|
||||
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.1);
|
||||
--launcher-pane-text-color: #000000bd;
|
||||
--launcher-pane-button-hover-color: black;
|
||||
--launcher-pane-button-hover-background: white;
|
||||
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
|
||||
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
|
||||
--launcher-pane-background-color: unset;
|
||||
--launcher-pane-text-color: unset;
|
||||
|
||||
--launcher-pane-vert-background-color: #e8e8e8;
|
||||
--launcher-pane-vert-text-color: #000000bd;
|
||||
--launcher-pane-vert-button-hover-color: black;
|
||||
--launcher-pane-vert-button-hover-background: white;
|
||||
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
|
||||
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.1);
|
||||
--launcher-pane-horiz-background-color: #fafafa;
|
||||
--launcher-pane-horiz-button-hover-color: black;
|
||||
--launcher-pane-horiz-button-hover-background: var(--icon-button-hover-background);
|
||||
--launcher-pane-horiz-button-hover-shadow: unset;
|
||||
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--protected-session-active-icon-color: #16b516;
|
||||
--sync-status-error-pulse-color: #ff5528;
|
||||
|
@ -26,11 +26,15 @@
|
||||
|
||||
--left-pane-item-selected-shadow-size: 2px;
|
||||
|
||||
--launcher-pane-size: 58px;
|
||||
--launcher-pane-horizontal-size: 54px;
|
||||
--launcher-pane-horizontal-icon-size: 20px;
|
||||
--launcher-pane-button-margin: 6px;
|
||||
--launcher-pane-button-gap: 3px;
|
||||
--launcher-pane-vert-size: 58px;
|
||||
--launcher-pane-vert-icon-size: 150%;
|
||||
--launcher-pane-vert-button-margin: 6px;
|
||||
--launcher-pane-vert-button-gap: 3px;
|
||||
|
||||
--launcher-pane-horiz-size: 54px;
|
||||
--launcher-pane-horiz-icon-size: 20px;
|
||||
--launcher-pane-horiz-button-margin: 8px;
|
||||
--launcher-pane-horiz-button-gap: 3px;
|
||||
|
||||
--tree-actions-toolbar-horizontal-margin: 8px;
|
||||
--tree-actions-toolbar-vertical-margin: 8px;
|
||||
|
@ -26,9 +26,7 @@ body {
|
||||
}
|
||||
|
||||
body.layout-horizontal {
|
||||
--launcher-pane-background-color: var(--launcher-pane-horizontal-background-color);
|
||||
--launcher-pane-size: var(--launcher-pane-horizontal-size);
|
||||
--active-tab-background-color: var(--launcher-pane-background-color);
|
||||
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
|
||||
--active-tab-hover-background-color: var(--active-tab-background-color);
|
||||
--new-tab-button-background: transparent;
|
||||
--tab-bar-height: 44px;
|
||||
@ -42,16 +40,19 @@ body.mobile {
|
||||
|
||||
/* #region Mica */
|
||||
body.background-effects.platform-win32 {
|
||||
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.15);
|
||||
--launcher-pane-background-color: rgba(255, 255, 255, 0.7);
|
||||
--tab-background-color: transparent;
|
||||
--new-tab-button-background: transparent;
|
||||
--active-tab-background-color: var(--launcher-pane-background-color);
|
||||
--active-tab-background-color: var(--launcher-pane-background-color); /* TODO: fix */
|
||||
--background-material: tabbed;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32 #launcher-pane {
|
||||
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.15);
|
||||
--launcher-pane-background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.background-effects.platform-win32 {
|
||||
body.background-effects.platform-win32 #launcher-pane {
|
||||
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.5);
|
||||
--launcher-pane-background-color: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
@ -59,11 +60,14 @@ body.background-effects.platform-win32 {
|
||||
|
||||
body.background-effects.platform-win32.layout-vertical {
|
||||
--left-pane-background-color: transparent;
|
||||
--launcher-pane-background-color: rgba(255, 255, 255, 0.055);
|
||||
--left-pane-item-hover-background: rgba(127, 127, 127, 0.05);
|
||||
--background-material: mica;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-vertical #launcher-pane {
|
||||
--launcher-pane-background-color: rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32,
|
||||
body.background-effects.platform-win32 #root-widget,
|
||||
body.background-effects.platform-win32 #launcher-pane .launcher-button {
|
||||
@ -141,18 +145,44 @@ body.layout-horizontal > .horizontal {
|
||||
}
|
||||
|
||||
#launcher-pane.vertical {
|
||||
--launcher-pane-border-color: unset;
|
||||
--launcher-pane-background-color: var(--launcher-pane-vert-background-color);
|
||||
--launcher-pane-text-color: var(--launcher-pane-vert-text-color);
|
||||
--launcher-pane-button-hover-color: var(--launcher-pane-vert-button-hover-color);
|
||||
--launcher-pane-button-hover-background: var(--launcher-pane-vert-button-hover-background);
|
||||
--launcher-pane-button-hover-shadow: var(--launcher-pane-vert-button-hover-shadow);
|
||||
--launcher-pane-button-focus-outline-color: var(--launcher-pane-vert-button-focus-outline-color);
|
||||
|
||||
--launcher-pane-size: var(--launcher-pane-vert-size);
|
||||
--launcher-pane-icon-size: var(--launcher-pane-vert-icon-size);
|
||||
--launcher-pane-button-margin: var(--launcher-pane-vert-button-margin);
|
||||
--launcher-pane-button-gap: var(--launcher-pane-vert-button-gap);
|
||||
|
||||
width: var(--launcher-pane-size) !important;
|
||||
padding-bottom: var(--launcher-pane-button-gap);
|
||||
}
|
||||
|
||||
#launcher-pane.horizontal {
|
||||
--launcher-pane-border-color: var(--launcher-pane-horiz-border-color);
|
||||
--launcher-pane-background-color: var(--launcher-pane-horiz-background-color);
|
||||
--launcher-pane-text-color: var(--launcher-pane-horiz-text-color);
|
||||
--launcher-pane-button-hover-color: var(--launcher-pane-horiz-button-hover-color);
|
||||
--launcher-pane-button-hover-background: var(--launcher-pane-horiz-button-hover-background);
|
||||
--launcher-pane-button-hover-shadow: var(--launcher-pane-horiz-button-hover-shadow);
|
||||
--launcher-pane-button-focus-outline-color: var(--launcher-pane-horiz-button-focus-outline-color);
|
||||
|
||||
--launcher-pane-size: var(--launcher-pane-horiz-size);
|
||||
--launcher-pane-icon-size: var(--launcher-pane-horiz-icon-size);
|
||||
--launcher-pane-button-margin: var(--launcher-pane-horiz-button-margin);
|
||||
--launcher-pane-button-gap: var(--launcher-pane-horiz-button-gap);
|
||||
|
||||
height: var(--launcher-pane-size) !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
#mobile-bottom-bar {
|
||||
background: var(--launcher-pane-background-color);
|
||||
background: var(--launcher-pane-horiz-background-color);
|
||||
}
|
||||
|
||||
body.mobile #launcher-pane {
|
||||
@ -163,7 +193,7 @@ body.layout-horizontal > .horizontal {
|
||||
@media (min-width: 992px) {
|
||||
#launcher-pane.horizontal {
|
||||
border-top: unset;
|
||||
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color);
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,6 +243,27 @@ body.layout-horizontal > .horizontal {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#launcher-pane .launcher-button {
|
||||
font-size: var(--launcher-pane-icon-size) !important;
|
||||
}
|
||||
|
||||
#launcher-pane .launcher-button:focus,
|
||||
#launcher-pane .global-menu button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#launcher-pane .launcher-button:focus-visible,
|
||||
#launcher-pane.horizontal .global-menu button:focus-visible {
|
||||
outline: 2px solid var(--launcher-pane-button-focus-outline-color);
|
||||
}
|
||||
|
||||
#launcher-pane.vertical .global-menu button:focus-visible svg {
|
||||
outline-offset: 4px;
|
||||
outline: 2px solid var(--launcher-pane-button-focus-outline-color);
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#launcher-pane.vertical .spacer {
|
||||
width: var(--launcher-pane-size);
|
||||
}
|
||||
@ -253,10 +304,6 @@ body.layout-horizontal > .horizontal {
|
||||
animation: sync-status-pulse 1s ease-in-out alternate-reverse infinite;
|
||||
}
|
||||
|
||||
#launcher-pane.horizontal .launcher-button {
|
||||
font-size: var(--launcher-pane-horizontal-icon-size);
|
||||
}
|
||||
|
||||
#launcher-pane .global-menu-button {
|
||||
--hover-item-background-color: transparent;
|
||||
}
|
||||
@ -711,7 +758,7 @@ body.layout-horizontal .tab-row-container .note-tab[active]:before {
|
||||
top: var(--tab-height);
|
||||
right: calc(100% - 1px);
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color);
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.layout-horizontal .tab-row-container .note-tab[active]:after {
|
||||
@ -723,7 +770,7 @@ body.layout-horizontal .tab-row-container .note-tab[active]:after {
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color);
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@ -757,7 +804,7 @@ body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-lay
|
||||
}
|
||||
|
||||
#root-widget.horizontal-layout .tab-row-widget .note-tab[active] .note-tab-wrapper {
|
||||
border: 1px solid var(--launcher-pane-horizontal-border-color);
|
||||
border: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
|
@ -193,4 +193,37 @@ describe("Markdown export", () => {
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("exports code in tables properly", () => {
|
||||
const html = trimIndentation`\
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Row 1
|
||||
</td>
|
||||
<td>
|
||||
<p>Allows displaying the value of one or more attributes in the calendar
|
||||
like this: </p>
|
||||
<p>
|
||||
<img src="13_Calendar View_image.png" alt="">
|
||||
</p>
|
||||
|
||||
<pre><code class="language-text-x-trilium-auto">#weight="70"
|
||||
#Mood="Good"
|
||||
#calendar:displayedAttributes="weight,Mood"</code></pre>
|
||||
<p>It can also be used with relations, case in which it will display the
|
||||
title of the target note:</p><pre><code class="language-text-x-trilium-auto">~assignee=@My assignee
|
||||
#calendar:displayedAttributes="assignee"</code></pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
const expected = trimIndentation`\
|
||||
<table><tbody><tr><td>Row 1</td><td><p>Allows displaying the value of one or more attributes in the calendar like this: </p><p><img src="13_Calendar View_image.png" alt=""></p><pre><code class="language-text-x-trilium-auto">#weight="70"
|
||||
#Mood="Good"
|
||||
#calendar:displayedAttributes="weight,Mood"</code></pre><p>It can also be used with relations, case in which it will display the title of the target note:</p><pre><code class="language-text-x-trilium-auto">~assignee=@My assignee
|
||||
#calendar:displayedAttributes="assignee"</code></pre></td></tr></tbody></table>`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import TurndownService from "turndown";
|
||||
import turndownPluginGfm from "@joplin/turndown-plugin-gfm";
|
||||
import { gfm } from "../../../packages/turndown-plugin-gfm/src/gfm.js";
|
||||
|
||||
let instance: TurndownService | null = null;
|
||||
|
||||
@ -43,7 +43,7 @@ function toMarkdown(content: string) {
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
instance.addRule("img", buildImageFilter());
|
||||
instance.addRule("admonition", buildAdmonitionFilter());
|
||||
instance.use(turndownPluginGfm.gfm);
|
||||
instance.use(gfm);
|
||||
instance.keep([ "kbd" ]);
|
||||
}
|
||||
|
||||
|