├── .npmrc ├── .eslintignore ├── versions.json ├── wrangler.toml ├── postcss.config.js ├── assets ├── README-1712821565619.png └── README-1712821625314.png ├── Gemfile ├── src ├── lib │ ├── codemirror.js │ ├── utils.ts │ ├── AppContext.ts │ ├── MermaidCrafter.ts │ └── Helper.ts ├── ReactContext.ts ├── component │ ├── Skeleton.tsx │ ├── Label.tsx │ ├── Separator.tsx │ ├── Checkbox.tsx │ ├── SmartGanttChart.tsx │ ├── Tooltip.tsx │ ├── Switch.tsx │ ├── RadioGroup.tsx │ ├── ScrollableList.tsx │ ├── ResizablePanel.tsx │ ├── Button.tsx │ ├── NavMenu.tsx │ ├── TaskList.tsx │ ├── Select.tsx │ └── SettingViewComponent.tsx ├── addon │ └── mode │ │ ├── multiplex.js │ │ ├── overlay.js │ │ ├── loadmode.js │ │ └── simple.js ├── sidebar │ ├── SmartGanttSibeBarView.tsx │ ├── SmartGanttSideBarReactComponent.tsx │ └── SidebarReactComponentNg.tsx ├── style │ └── src.css ├── BlockComponent │ ├── NavBar.tsx │ ├── SmartGanttBlockReactComponentNg.tsx │ ├── TaskListMdBlock.tsx │ └── SmartGanttBlockReactComponent.tsx ├── HelperNg.tsx ├── SettingManager.ts ├── GanttBlockManager.tsx ├── TimelineExtractor.ts ├── FilterModal.ts ├── MarkdownProcesser.ts ├── GanttItemView.tsx └── mode │ └── gantt │ └── gantt-list.js ├── obsidian-smart-gantt-document ├── attachments │ ├── Pasted image 20240527153131.png │ ├── Pasted image 20240527153230.png │ ├── Pasted image 20240527153238.png │ └── Pasted image 20240527154015.png └── index.md ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── LICENSE ├── esbuild.config.mjs ├── .github └── workflows │ ├── release.yml │ └── doc.yml ├── generate_Random_tasks.mjs ├── quartz.layout.ts ├── script └── gen-sample-md.mjs ├── README.md ├── tailwind.config.js ├── Gemfile.lock ├── quartz.config.ts ├── package.json ├── main.tsx └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0" 3 | } 4 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name="obsidian-smart-gantt" 2 | pages_build_output_dir="public" 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /assets/README-1712821565619.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/assets/README-1712821565619.png -------------------------------------------------------------------------------- /assets/README-1712821625314.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/assets/README-1712821625314.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # gem "rails" 6 | 7 | gem "github_changelog_generator", "~> 1.16" 8 | -------------------------------------------------------------------------------- /src/lib/codemirror.js: -------------------------------------------------------------------------------- 1 | // Never import or require codemirror; the solution is we using the built-in codemirror in obsidian. Don't let esbuild built them 2 | module.exports = CodeMirror; 3 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /obsidian-smart-gantt-document/attachments/Pasted image 20240527153131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/obsidian-smart-gantt-document/attachments/Pasted image 20240527153131.png -------------------------------------------------------------------------------- /obsidian-smart-gantt-document/attachments/Pasted image 20240527153230.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/obsidian-smart-gantt-document/attachments/Pasted image 20240527153230.png -------------------------------------------------------------------------------- /obsidian-smart-gantt-document/attachments/Pasted image 20240527153238.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/obsidian-smart-gantt-document/attachments/Pasted image 20240527153238.png -------------------------------------------------------------------------------- /obsidian-smart-gantt-document/attachments/Pasted image 20240527154015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhannht/obsidian-smart-gantt/HEAD/obsidian-smart-gantt-document/attachments/Pasted image 20240527154015.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /src/lib/AppContext.ts: -------------------------------------------------------------------------------- 1 | import {App} from "obsidian"; 2 | import {createContext, useContext} from "react"; 3 | 4 | export const AppContext = createContext(undefined); 5 | 6 | export function useApp(): App | undefined { 7 | return useContext(AppContext) 8 | } 9 | -------------------------------------------------------------------------------- /src/ReactContext.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from "react"; 2 | import SmartGanttPlugin from "../main"; 3 | export const PluginContext = 4 | createContext(undefined) 5 | 6 | export const usePlugin = ():SmartGanttPlugin|undefined=>{ 7 | return useContext(PluginContext) 8 | } 9 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "smart-gantt", 3 | "name": "Smart Gantt", 4 | "version": "0.1.17", 5 | "minAppVersion": "0.1.5", 6 | "description": "Intelligently generate Gantt chart from your tasks", 7 | "author": "Nhan Nguyen", 8 | "authorUrl": "https://github.com/nhannht", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /src/component/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/addon/mode/multiplex.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function (mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function (CodeMirror) { 12 | "use strict"; 13 | }); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | /styles.css 24 | /main.css 25 | /obsidian-smart-gantt-document/.obsidian/ 26 | /script/test-remark.mjs 27 | /script/test-tree-shitter.mjs 28 | /tasks_markdown.md 29 | /tasks_sample.json 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "inlineSourceMap": true, 6 | "inlineSources": true, 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "allowJs": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "strictNullChecks": true, 15 | "jsx": "react-jsx", 16 | "lib": [ 17 | "DOM", 18 | "ES5", 19 | "ES6", 20 | "ES7" 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": [ 27 | "**/*.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /src/component/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/component/Separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024-The end of the world - Nhan Nht 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /src/addon/mode/overlay.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | // Utility function that allows modes to be combined. The mode given 5 | // as the base argument takes care of most of the normal mode 6 | // functionality, but a second (typically simple) mode is used, which 7 | // can override the style of text. Both modes get to parse all of the 8 | // text, but when both assign a non-null style to a piece of code, the 9 | // overlay wins, unless the combine argument was true and not overridden, 10 | // or state.overlay.combineTokens was true, in which case the styles are 11 | // combined. 12 | 13 | (function (mod) { 14 | if (typeof exports == "object" && typeof module == "object") // CommonJS 15 | mod(require("../../lib/codemirror")); 16 | else if (typeof define == "function" && define.amd) // AMD 17 | define(["../../lib/codemirror"], mod); 18 | else // Plain browser env 19 | mod(CodeMirror); 20 | })(function (CodeMirror) { 21 | "use strict"; 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["main.tsx"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /src/component/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { Check } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | 8 | const Checkbox = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 23 | 24 | 25 | 26 | )) 27 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 28 | 29 | export { Checkbox } 30 | -------------------------------------------------------------------------------- /src/component/SmartGanttChart.tsx: -------------------------------------------------------------------------------- 1 | import {TimelineExtractorResultNg} from "@/TimelineExtractor"; 2 | import {Gantt, Task} from "gantt-task-react"; 3 | import SmartGanttPlugin from "../../main"; 4 | import {SmartGanttSettings} from "@/SettingManager"; 5 | // const CustomToolTip = (props:{ 6 | // task:Task, 7 | // fontSize:string, 8 | // fontFamily:string 9 | // })=> { 10 | // return <> 11 | //
12 | // {props.task.name} 13 | //
14 | // 15 | // } 16 | 17 | const SmartGanttChart = (props: { 18 | tasks: Task[], 19 | results: TimelineExtractorResultNg[] 20 | thisPlugin: SmartGanttPlugin 21 | settings: SmartGanttSettings | undefined 22 | }) => { 23 | 24 | let listCellWidthAttr = null 25 | if (!props.settings?.leftBarChartDisplayQ) { 26 | listCellWidthAttr = { 27 | listCellWidth: "" 28 | } 29 | } else { 30 | listCellWidthAttr = {} 31 | } 32 | 33 | return { 35 | await props.thisPlugin.helper.jumpToPositionOfNode(t, props.results) 36 | }} 37 | tasks={props.tasks} 38 | // listCellWidth="" 39 | viewMode={props.settings?.viewMode} 40 | {...listCellWidthAttr} 41 | ganttHeight={300} 42 | // TooltipContent={CustomToolTip} 43 | /> 44 | 45 | 46 | } 47 | 48 | export default SmartGanttChart 49 | -------------------------------------------------------------------------------- /src/component/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/component/Switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "../lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | if: "contains(github.event.head_commit.message,'build:')" 11 | permissions: write-all 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | 19 | - name: set up npm 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '20' 23 | 24 | # - name: "📔 Generate a changelog" 25 | # uses: orhun/git-cliff-action@v3 26 | # with: 27 | # config: cliff.toml 28 | # args: --verbose --latest 29 | # env: 30 | # OUTPUT: CHANGELOG_temp.md 31 | 32 | - name: Build plugin 33 | run: | 34 | yarn 35 | 36 | yarn run build 37 | 38 | - name: "Get the tag" 39 | id: get_tag 40 | run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} 41 | 42 | - name: Create release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | run: | 46 | tag=${{ steps.get_tag.outputs.tag }} 47 | 48 | gh release create "$tag" \ 49 | --title="$tag" \ 50 | --draft \ 51 | main.js manifest.json styles.css LICENSE README.md 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /generate_Random_tasks.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {faker} from "@faker-js/faker"; 3 | import {writeFileSync} from "fs"; 4 | import {v4 as uuidv4} from "uuid" 5 | import moment from "moment" 6 | 7 | const allTaskId = faker.helpers.uniqueArray(uuidv4,20) 8 | // console.log(allTaskId) 9 | 10 | const dateFormat = "YYYY-MM-DD" 11 | 12 | function generateTask (id){ 13 | return { 14 | id: id, 15 | content:faker.word.words(10), 16 | start: moment(faker.date.recent({days:60})).format("YYYY-MM-DD"), 17 | due: moment(faker.date.soon()).format("YYYY-MM-DD"), 18 | dependences: faker.helpers.arrayElements(allTaskId,{min:0,max:3}), 19 | progress: faker.number.int({min: 0, max: 100}), 20 | type: faker.helpers.arrayElement(["task","milestone","project"]), 21 | inventory: faker.helpers.arrayElement(["task","backlog"]) 22 | } 23 | } 24 | let results = [] 25 | allTaskId.forEach((id, index) => { 26 | const task = generateTask(id) 27 | results.push(task) 28 | }) 29 | 30 | let markdownTaskString = [] 31 | results.forEach((task,id)=>{ 32 | markdownTaskString.push(`- [ ] ${task.content} [smartGanttId :: ${task.id}] [start :: ${task.start}] [due :: ${task.due}] [progress:: ${task.progress}] [dependencies :: ${task.dependences}] [type :: ${task.type}]`) 33 | }) 34 | 35 | writeFileSync("tasks_sample.json",JSON.stringify(results,null,"\t")) 36 | writeFileSync("tasks_markdown.md",markdownTaskString.join("\n") + "\n") 37 | -------------------------------------------------------------------------------- /src/sidebar/SmartGanttSibeBarView.tsx: -------------------------------------------------------------------------------- 1 | import {IconName, ItemView, WorkspaceLeaf} from "obsidian"; 2 | import {createRoot, Root} from "react-dom/client"; 3 | import SmartGanttPlugin from "../../main"; 4 | import "../../styles.css" 5 | import SidebarReactComponentNg from "./SidebarReactComponentNg"; 6 | 7 | 8 | /** 9 | * The api of obsidian about custom view is a bit complicated to use. 10 | * View and leaf is two different term. View must be regist via a "ViewCreator" function that return a view and everyview must be assicate with a type. 11 | * That type act like an id that other term, like leaf, will bind the view state using that id 12 | */ 13 | export default class SmartGanttSibeBarView extends ItemView { 14 | 15 | 16 | root: Root | null = null; 17 | 18 | 19 | constructor(leaf: WorkspaceLeaf, 20 | private thisPlugin: SmartGanttPlugin 21 | ) { 22 | super(leaf); 23 | 24 | } 25 | 26 | getViewType() { 27 | return "smart-gantt" 28 | } 29 | 30 | getDisplayText() { 31 | return "Smart Gantt"; 32 | } 33 | 34 | 35 | 36 | 37 | override async onOpen() { 38 | this.root = createRoot(this.containerEl.children[1]); 39 | 40 | 41 | this.root.render( 42 | // 43 | 44 | ) 45 | 46 | } 47 | 48 | override async onClose() { 49 | this.root?.unmount() 50 | 51 | } 52 | 53 | override getIcon(): IconName { 54 | return 'egg' 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/component/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 4 | import { Circle } from "lucide-react" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const RadioGroup = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => { 12 | return ( 13 | 18 | ) 19 | }) 20 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 21 | 22 | const RadioGroupItem = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => { 26 | return ( 27 | 35 | 36 | 37 | 38 | 39 | ) 40 | }) 41 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 42 | 43 | export { RadioGroup, RadioGroupItem } 44 | -------------------------------------------------------------------------------- /src/sidebar/SmartGanttSideBarReactComponent.tsx: -------------------------------------------------------------------------------- 1 | // import {useApp} from "./AppContext"; 2 | import {loadMermaid, MarkdownPostProcessorContext} from "obsidian"; 3 | import SmartGanttPlugin from "../../main"; 4 | import {useMeasure} from "react-use"; 5 | 6 | /** 7 | * Very old component, not exposed anymore and better not touch this sh*t 8 | * @param props 9 | * @constructor 10 | */ 11 | export const SmartGanttSideBarReactComponent = (props: { 12 | mermaidCraft: string, 13 | ctx?: MarkdownPostProcessorContext, 14 | src?: string, 15 | thisPlugin?: SmartGanttPlugin, 16 | }) => { 17 | 18 | 19 | if (props.mermaidCraft === "") { 20 | return <> 21 |
Oops, you need at least one task with time can be parse for the Gantt chart being show
22 |
Trying add a task like: "- [ ] feed my kitty tomorrow" in your editing file
23 | 24 | } 25 | 26 | const [mainContainerRef , mainContainerMeasure] = useMeasure() 27 | 28 | 29 | const mermaidSvgComponent = ()=>{ 30 | if (mainContainerMeasure.width <=0){ 31 | return <> 32 | } 33 | 34 | loadMermaid().then((mermaid) => { 35 | mermaid.initialize({ 36 | startOnLoad: true, 37 | maxTextSize: 99999999, 38 | }); 39 | mermaid.contentLoaded(); 40 | }) 41 | 42 | return ( 43 |
44 |
45 | 				{props.mermaidCraft}
46 | 			
47 |
48 | ) 49 | } 50 | 51 | 52 | let mainComponent = () => { 53 | // @ts-ignore 54 | return
55 | {mermaidSvgComponent()} 56 |
57 | 58 | } 59 | 60 | 61 | 62 | return <> 63 | {mainComponent()} 64 | 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /quartz.layout.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {PageLayout, SharedLayout} from "./quartz/cfg" 3 | // @ts-ignore 4 | import * as Component from "./quartz/components" 5 | 6 | // components shared across all pages 7 | export const sharedPageComponents: SharedLayout = { 8 | head: Component.Head(), 9 | header: [], 10 | footer: Component.Footer( 11 | { 12 | links: { 13 | "Source code": "https://github.com/nhannht/obsidian-smart-gantt", 14 | "RSS": "https://obsidian-smartt-gantt.pages.dev/index.xml" 15 | , 16 | }, 17 | }), 18 | } 19 | 20 | // components for pages that display a single page (e.g. a single note) 21 | export const defaultContentPageLayout: PageLayout = { 22 | beforeBody: [ 23 | Component.Breadcrumbs(), 24 | Component.ArticleTitle(), 25 | Component.ContentMeta(), 26 | Component.TagList(), 27 | ], 28 | left: [ 29 | Component.PageTitle(), 30 | Component.MobileOnly(Component.Spacer()), 31 | Component.Search(), 32 | Component.Darkmode(), 33 | Component.DesktopOnly(Component.Explorer()), 34 | ], 35 | right: [ 36 | Component.Graph(), 37 | Component.DesktopOnly(Component.TableOfContents()), 38 | Component.Backlinks(), 39 | ], 40 | // footer: Component.Discussion() 41 | 42 | 43 | } 44 | 45 | // components for pages that display lists of pages (e.g. tags or folders) 46 | export const defaultListPageLayout: PageLayout = { 47 | beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], 48 | left: [ 49 | Component.PageTitle(), 50 | Component.MobileOnly(Component.Spacer()), 51 | Component.Search(), 52 | Component.Darkmode(), 53 | Component.DesktopOnly(Component.Explorer()), 54 | ], 55 | right: [], 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: build doc 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | 8 | jobs: 9 | build-doc: 10 | runs-on: ubuntu-latest 11 | if: "contains(github.event.head_commit.message,'doc:')" 12 | permissions: write-all 13 | steps: 14 | - name: "check out quartz custom template " 15 | uses: actions/checkout@v4 16 | with: 17 | repository: "nhannht/quartz" 18 | fetch-depth: 0 19 | ref: nhannht 20 | path: quartz 21 | 22 | - name: check out this repo 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | path: obsidian-smart-gantt 27 | 28 | 29 | - name: set up node 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '20' 33 | 34 | - name: build and deploy doc 35 | env: 36 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 37 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 38 | run: | 39 | cd quartz 40 | npm install 41 | npm install wrangler 42 | cp ../obsidian-smart-gantt/obsidian-smart-gantt-document/* ./content/ -r 43 | cp ../obsidian-smart-gantt/quartz.config.ts ./ 44 | cp ../obsidian-smart-gantt/quartz.layout.ts ./ 45 | cp ../obsidian-smart-gantt/wrangler.toml ./ 46 | npx quartz build 47 | npx wrangler pages deploy --branch=master 48 | -------------------------------------------------------------------------------- /obsidian-smart-gantt-document/index.md: -------------------------------------------------------------------------------- 1 | - `Gantt` is a cool way to visualize task. 2 | 3 | - Most of time I care about write out my task, quickly jump to them and check them when their are done. That all. But anyone want to see cool things. Good, just look at the visualize, it is generation using mermaid. 4 | - I want to using more mature library. But how on earth noone design an open source Javascript library to generate Gantt chart. 5 | 6 | ### How to use it. 7 | - Just write your task, any valid task need to be at least a list item + a checkbox. 8 | ![[Pasted image 20240527153131.png]] 9 | 10 | 11 | 12 | - There is a button, click on it. A sidebar appear in the right. We have "magic" appear here. That all, this is the simply thing I want to do with this plugin. 13 | 14 | ![[Pasted image 20240527153238.png]] 15 | 16 | - But for someone want to visualize task. Add some time range to your task. 17 | 18 | ![[Pasted image 20240527154015.png]]- Mermaid visualize look ugly and bold, that all. But that is the best I can find. And at least it is fast and lightweight. 19 | 20 | > [!tip] 21 | > 22 | > Only track a valid task (line with checkbox) which have part of string that can interpret as time/time range 23 | > 24 | > Smart Gantt is not perfect for natural language processing: 25 | > - Cannot parse text with only a year like "2024", so please write your sentence a bit clearly 26 | > 27 | > - Time (hours, minutes) of day must stay after date. Example Sat Aug 17 2024 9 AM or Sat Aug 17 2013 18:40:39 GMT+0900 or 2014-11-30T08:15:30-05:30. But 9 AM April/11/2024 will be parsed as 2 different points of time. 28 | > 29 | > - Relative time like - today, tomorrow, yesterday, last friday, 5 hours from now - will work in theory, but it is not useful at all. Because every time you refresh the plot will parse from your current point of time 30 | 31 | -------------------------------------------------------------------------------- /script/gen-sample-md.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals' 3 | import {mkdirSync, writeFileSync} from "fs"; 4 | import {existsSync, rm, rmdirSync} from "node:fs"; 5 | import {faker} from '@faker-js/faker' 6 | 7 | const vaultName = argv.vault ? argv.vault : (() => { 8 | // console.log("need --vault name argument") 9 | process.exit(0) 10 | })() 11 | 12 | if (!existsSync((vaultName))) { 13 | mkdirSync(vaultName) 14 | } else { 15 | rmdirSync(vaultName, {recursive: true}) 16 | mkdirSync(vaultName) 17 | } 18 | 19 | const numberOfFiles = argv.fileN ? argv.fileN : 1 20 | const numberOfEl = argv.elN ? argv.elN : 10 21 | 22 | 23 | 24 | const filePaths = faker.helpers.uniqueArray(()=>{ 25 | return path.join(vaultName,faker.system.directoryPath(),faker.system.commonFileName("md")) 26 | },numberOfFiles) 27 | 28 | 29 | 30 | function generateOneTaskOrParagraphs(){ 31 | return faker.helpers.weightedArrayElement([ 32 | { 33 | weight: 3, 34 | value: `- [ ] ${faker.lorem.slug()}\n` 35 | }, 36 | { 37 | weight: 3, 38 | value: `${faker.lorem.paragraphs({min: 1, max: 5}, "\n")}\n` 39 | }, 40 | { 41 | weight: 2, 42 | value: (()=> { 43 | let r = faker.date.recent().toLocaleString() 44 | // console.log(r) 45 | let s = faker.date.soon().toLocaleString() 46 | 47 | return `\n- [ ] ${faker.lorem.slug()} from ${r} to ${s}\n` 48 | })() 49 | }, 50 | { 51 | weight: 2, 52 | value: `- [ ] ${faker.lorem.slug()} at ${faker.date.soon().toLocaleString()}\n` 53 | } 54 | ]) 55 | 56 | } 57 | 58 | filePaths.forEach((f, i) => { 59 | let element = [] 60 | for (let i = 0; i< numberOfEl;i++){ 61 | element.push(generateOneTaskOrParagraphs()) 62 | } 63 | const fileDir = path.dirname(f) 64 | if (!existsSync(fileDir)) mkdirSync(fileDir,{recursive:true}) 65 | writeFileSync(`${f}`,element.join("\n")) 66 | 67 | }) 68 | 69 | -------------------------------------------------------------------------------- /src/component/ScrollableList.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const ScrollArea = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, children, ...props }, ref) => ( 11 | 16 | 17 | {children} 18 | 19 | 20 | 21 | 22 | )) 23 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 24 | 25 | const ScrollBar = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, orientation = "vertical", ...props }, ref) => ( 29 | 42 | 43 | 44 | )) 45 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 46 | 47 | export { ScrollArea, ScrollBar } 48 | -------------------------------------------------------------------------------- /src/component/ResizablePanel.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { GripVertical } from "lucide-react" 4 | import * as ResizablePrimitive from "react-resizable-panels" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 19 | ) 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps & { 28 | withHandle?: boolean 29 | }) => ( 30 | div]:rotate-90", 33 | className 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ) 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >[!tip] 2 | > Check https://obsidian-smart-gantt.pages.dev for a full document 3 | 4 | 5 |

Obsidian Smart Gantt

8 | 9 | --- 10 | 11 |
12 | Intelligently generate Gantt Chart for your task across your vault 13 | 14 |
15 | 16 | --- 17 | 18 |
    19 | 20 | - Keep track of all your tasks across your vault. 21 | - Generate a Gantt chart based on them 22 | - Quick jump to your task location. 23 | 24 |
25 | 26 | --- 27 | 28 | ##### Simplest use cases 29 | 30 | --- 31 | 32 | ###### Using the right sidebar. 33 | 34 | - Open your sidebar and magic will happen 35 | 36 | ![](./assets/README-1712821565619.png) 37 | 38 | ###### Gantt code block. 39 | 40 | 41 | 42 | --- 43 | 44 |
Simply create a Gantt code block somewhere in your file
45 | 46 | ````markdown 47 | ```gantt 48 | 49 | ``` 50 | ```` 51 | 52 | ![](./assets/README-1712821625314.png) 53 | 54 | --- 55 | 56 | >[!note] 57 | > Right click/Hold (on mobile) the plot to open the settings view. All the settings will be stored as JSON in your Gantt block 58 | > 59 | > You can also edit the settings manually, Smart Gantt using JSON as domain language. 60 | 61 | ##### Limitation 62 | 63 | 64 | 65 | > [!tip] 66 | > 67 | > Only track a valid task (line with checkbox) which have part of string that can interpret as time/time range 68 | > 69 | > Smart Gantt is not perfect for natural language processing: 70 | > - Cannot parse text with only a year like "2024", so please write your sentence a bit clearly 71 | > 72 | > - Time (hours, minutes) of day must stay after date. Example Sat Aug 17 2024 9 AM or Sat Aug 17 2013 18:40:39 GMT+0900 or 2014-11-30T08:15:30-05:30. But 9 AM April/11/2024 will be parsed as 2 different points of time. 73 | > 74 | > - Relative time like - today, tomorrow, yesterday, last friday, 5 hours from now - will work in theory, but it is not useful at all. Because every time you refresh the plot will parse from your current point of time 75 | -------------------------------------------------------------------------------- /src/component/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/style/src.css: -------------------------------------------------------------------------------- 1 | @import "gantt-task-react/dist/index.css"; 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | 7 | @layer base { 8 | :root { 9 | --background: 0 0% 100%; 10 | --foreground: 222.2 47.4% 11.2%; 11 | 12 | --muted: 210 40% 96.1%; 13 | --muted-foreground: 215.4 16.3% 46.9%; 14 | 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 222.2 47.4% 11.2%; 17 | 18 | --border: 214.3 31.8% 91.4%; 19 | --input: 214.3 31.8% 91.4%; 20 | 21 | --card: 0 0% 100%; 22 | --card-foreground: 222.2 47.4% 11.2%; 23 | 24 | --primary: 222.2 47.4% 11.2%; 25 | --primary-foreground: 210 40% 98%; 26 | 27 | --secondary: 210 40% 96.1%; 28 | --secondary-foreground: 222.2 47.4% 11.2%; 29 | 30 | --accent: 210 40% 96.1%; 31 | --accent-foreground: 222.2 47.4% 11.2%; 32 | 33 | --destructive: 0 100% 50%; 34 | --destructive-foreground: 210 40% 98%; 35 | 36 | --ring: 215 20.2% 65.1%; 37 | 38 | --radius: 0.5rem; 39 | } 40 | 41 | .dark { 42 | --background: 224 71% 4%; 43 | --foreground: 213 31% 91%; 44 | 45 | --muted: 223 47% 11%; 46 | --muted-foreground: 215.4 16.3% 56.9%; 47 | 48 | --accent: 216 34% 17%; 49 | --accent-foreground: 210 40% 98%; 50 | 51 | --popover: 224 71% 4%; 52 | --popover-foreground: 215 20.2% 65.1%; 53 | 54 | --border: 216 34% 17%; 55 | --input: 216 34% 17%; 56 | 57 | --card: 224 71% 4%; 58 | --card-foreground: 213 31% 91%; 59 | 60 | --primary: 210 40% 98%; 61 | --primary-foreground: 222.2 47.4% 1.2%; 62 | 63 | --secondary: 222.2 47.4% 11.2%; 64 | --secondary-foreground: 210 40% 98%; 65 | 66 | --destructive: 0 63% 31%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --ring: 216 34% 17%; 70 | 71 | --radius: 0.5rem; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-background text-foreground; 81 | font-feature-settings: "rlig" 1, "calt" 1; 82 | } 83 | } 84 | 85 | [hidden]{ 86 | display: none !important; 87 | } 88 | -------------------------------------------------------------------------------- /src/BlockComponent/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../../main"; 2 | import { 3 | NavigationMenu, 4 | NavigationMenuItem, 5 | NavigationMenuLink, 6 | NavigationMenuList, 7 | navigationMenuTriggerStyle 8 | } from "@/component/NavMenu"; 9 | import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/component/Tooltip"; 10 | 11 | export const NavBar = (props: { 12 | setIsSettingQFn: (status: boolean) => void 13 | thisPlugin?: SmartGanttPlugin, 14 | reloadViewButtonQ?: boolean, 15 | }) => { 16 | let reloadButton: JSX.Element 17 | if ("reloadViewButtonQ" in props && "thisPlugin" in props && props.reloadViewButtonQ === true) { 18 | reloadButton = 19 | 20 | 21 | 22 | { 25 | setTimeout(()=>props.thisPlugin?.helper.reloadView(),2000) 26 | }}> 27 | Reload 28 | 29 | 30 | 31 |
32 |
33 | The change from inside this plugin (sidebar/block) will affect outside.
34 | But any change from the outside (eg. you edit the file) will not auto trigger the 35 | update.
36 | So please click this button to manual update 37 |
38 |
This button will wait 2s before reload, because Obsidian
39 | not autosave your change immediately after you make change 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 | } else { 48 | reloadButton = <> 49 | } 50 | 51 | return 52 | 53 | 54 | props.setIsSettingQFn(true)} 56 | className={navigationMenuTriggerStyle()}>Settings 57 | 58 | {reloadButton} 59 | 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/HelperNg.tsx: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../main"; 2 | import {remark} from "remark"; 3 | import remarkGfm from "remark-gfm"; 4 | import remarkParse from "remark-parse"; 5 | import {TFile} from "obsidian"; 6 | import wikiLinkPlugin from "remark-wiki-link"; 7 | 8 | 9 | export type TaskWithMetaData = { 10 | name: string, 11 | checkbox: boolean, 12 | metadata: { 13 | [key: string]: string 14 | 15 | }, 16 | lineIndex: number|null 17 | } 18 | 19 | export default class HelperNg { 20 | private remarkProcessor; 21 | 22 | constructor(public plugin: SmartGanttPlugin) { 23 | this.remarkProcessor = remark().use(remarkGfm).use(remarkParse).use(wikiLinkPlugin) 24 | } 25 | 26 | async getAllLinesContainCheckboxInMarkdown(file: TFile) { 27 | // console.log("we are in the helper") 28 | // console.log("before read file") 29 | // console.log(file) 30 | 31 | const fileContent = await this.plugin.app.vault.read(file) 32 | // console.log("after read file conent") 33 | let results: { lineContent: string; lineIndex: number; }[] = [] 34 | const lines = fileContent.split("\n") 35 | const regexForTask = /^- \[([ |x])] (.+)$/ 36 | lines.forEach((line,index)=>{ 37 | if (line.trim().match(regexForTask)){ 38 | results.push({ 39 | 'lineContent': line, 40 | 'lineIndex': index 41 | }) 42 | 43 | } 44 | }) 45 | 46 | 47 | return results 48 | } 49 | 50 | async extractLineWithCheckboxToTaskWithMetaData(task:{lineContent:string,lineIndex:number}) { 51 | const regex = /- \[(x| )\] (.*?)(\[.*\])/ 52 | const matches = task.lineContent.match(regex); 53 | // console.log(matches) 54 | 55 | if (matches) { 56 | const checkbox = matches[1] === 'x'; 57 | const name = matches[2].trim(); 58 | const keyValueRegex = /\[([^\[\]]*?)::([^\[\]]*?)]/g; 59 | const keyValuePairs:{[key:string]:string} = {} 60 | 61 | let match; 62 | while ((match = keyValueRegex.exec(matches[3])) !== null) { 63 | keyValuePairs[match[1].trim()] = match[2].trim() 64 | 65 | } 66 | 67 | return { 68 | checkbox, 69 | name, 70 | metadata: keyValuePairs, 71 | lineIndex: task.lineIndex 72 | } as TaskWithMetaData 73 | } 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/MermaidCrafter.ts: -------------------------------------------------------------------------------- 1 | import {TimelineExtractorResult} from "../TimelineExtractor"; 2 | import * as AES from "crypto-js/aes"; 3 | import SmartGanttPlugin from "../../main"; 4 | 5 | export default class MermaidCrafter { 6 | private _thisPlugin: SmartGanttPlugin; 7 | get thisPlugin(): SmartGanttPlugin { 8 | return this._thisPlugin; 9 | } 10 | 11 | set thisPlugin(value: SmartGanttPlugin) { 12 | this._thisPlugin = value; 13 | } 14 | 15 | constructor(thisPlugin: SmartGanttPlugin) { 16 | this._thisPlugin = thisPlugin; 17 | } 18 | 19 | craftMermaid(results: TimelineExtractorResult[]) { 20 | let craft = "" 21 | craft += "gantt\n" 22 | // craft += "title Gantt diagram\n" 23 | craft += "dateFormat YYYY-MM-DD\n" 24 | let taskIdMap = new Map(); 25 | 26 | results.forEach(result => { 27 | craft += `\tsection ${result.file.basename}\n` 28 | if (result.parsedResultsAndRawText.parsedResults){ 29 | result.parsedResultsAndRawText.parsedResults.forEach(parseResult => { 30 | const startDateString = parseResult.start.date().toDateString() 31 | const taskId = AES.encrypt(parseResult.start.date().toDateString(), "secret") 32 | taskIdMap.set(taskId, { 33 | file: result.file.basename, 34 | vault: result.file.vault.getName() 35 | }) 36 | let checked = false 37 | if ("checked" in result.token) { 38 | checked = result.token.checked 39 | } 40 | 41 | let diff = 1 42 | if (parseResult.end) { 43 | diff = Math.round(Math.abs(parseResult.end.date().getTime() - parseResult.start.date().getTime()) / 86400000) 44 | } 45 | 46 | if ("text" in result.token) { 47 | let text = result.token.text.trim().split("\n")[0] 48 | text = text.replace(parseResult.text, "").trim() 49 | text = text.replace(/:/g,"🐱") 50 | text = text.replace(/#/g, "😺") 51 | // text = text.replace(/+/g, "😺" ) 52 | // text = text.replace(/-/g, "" ) 53 | craft += `\t\t${text}:\t ${checked? "done ," : "active ,"} ${taskId},${startDateString}, ${diff}d\n` 54 | // console.log(craft) 55 | } 56 | }) 57 | 58 | } 59 | 60 | }) 61 | 62 | taskIdMap.forEach((value, key) => { 63 | const urlWhenClick = encodeURI("obsidian://open?vault=" + value.vault + "&file=" + value.file) 64 | craft += `\tclick ${key} href "${urlWhenClick}"\n` 65 | }) 66 | return craft 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { scopedPreflightStyles, isolateInsideOfContainer } from 'tailwindcss-scoped-preflight'; 2 | 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | content: [ 7 | "./src/**/*.{ts,tsx,html}", 8 | "./main.{ts,tsx}" 9 | ], 10 | safelist: [ 11 | "opacity-50" 12 | ], 13 | important: true, 14 | plugins: [ 15 | require("tailwindcss-animate"), 16 | scopedPreflightStyles({ 17 | isolationStrategy: isolateInsideOfContainer('.twp') 18 | }) 19 | 20 | ], 21 | darkMode: ["class"], 22 | theme: { 23 | container: { 24 | center: true, 25 | padding: "2rem", 26 | screens: { 27 | "2xl": "1400px", 28 | }, 29 | }, 30 | extend: { 31 | colors: { 32 | border: "hsl(var(--border))", 33 | input: "hsl(var(--input))", 34 | ring: "hsl(var(--ring))", 35 | background: "hsl(var(--background))", 36 | foreground: "hsl(var(--foreground))", 37 | primary: { 38 | DEFAULT: "hsl(var(--primary))", 39 | foreground: "hsl(var(--primary-foreground))", 40 | }, 41 | secondary: { 42 | DEFAULT: "hsl(var(--secondary))", 43 | foreground: "hsl(var(--secondary-foreground))", 44 | }, 45 | destructive: { 46 | DEFAULT: "hsl(var(--destructive))", 47 | foreground: "hsl(var(--destructive-foreground))", 48 | }, 49 | muted: { 50 | DEFAULT: "hsl(var(--muted))", 51 | foreground: "hsl(var(--muted-foreground))", 52 | }, 53 | accent: { 54 | DEFAULT: "hsl(var(--accent))", 55 | foreground: "hsl(var(--accent-foreground))", 56 | }, 57 | popover: { 58 | DEFAULT: "hsl(var(--popover))", 59 | foreground: "hsl(var(--popover-foreground))", 60 | }, 61 | card: { 62 | DEFAULT: "hsl(var(--card))", 63 | foreground: "hsl(var(--card-foreground))", 64 | }, 65 | }, 66 | borderRadius: { 67 | lg: `var(--radius)`, 68 | md: `calc(var(--radius) - 2px)`, 69 | sm: "calc(var(--radius) - 4px)", 70 | }, 71 | keyframes: { 72 | "accordion-down": { 73 | from: {height: "0"}, 74 | to: {height: "var(--radix-accordion-content-height)"}, 75 | }, 76 | "accordion-up": { 77 | from: {height: "var(--radix-accordion-content-height)"}, 78 | to: {height: "0"}, 79 | }, 80 | }, 81 | animation: { 82 | "accordion-down": "accordion-down 0.2s ease-out", 83 | "accordion-up": "accordion-up 0.2s ease-out", 84 | }, 85 | }, 86 | }, 87 | }; 88 | 89 | -------------------------------------------------------------------------------- /src/SettingManager.ts: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../main"; 2 | import {ViewMode} from "gantt-task-react"; 3 | 4 | export interface SmartGanttSettings { 5 | pathListFilter: String[], 6 | todoShowQ: boolean, 7 | doneShowQ: boolean, 8 | viewMode: ViewMode, 9 | leftBarChartDisplayQ :boolean, 10 | // viewDate?: Date, 11 | 12 | 13 | } 14 | 15 | export default class SettingManager { 16 | get settings(): SmartGanttSettings { 17 | return this._settings; 18 | } 19 | 20 | set settings(value: SmartGanttSettings) { 21 | this._settings = value; 22 | } 23 | 24 | constructor(private thisPlugin: SmartGanttPlugin, 25 | private _settings: SmartGanttSettings) { 26 | 27 | } 28 | 29 | async loadSettings() { 30 | const vaultName = this.thisPlugin.app.vault.getName() 31 | if (localStorage.getItem(`smart-gantt-settings-${vaultName}`)) { 32 | this._settings = Object.assign( 33 | {}, 34 | this._settings, 35 | //@ts-ignore 36 | JSON.parse(localStorage.getItem(`smart-gantt-settings-${vaultName}`))) 37 | } 38 | } 39 | 40 | async saveSettings(newSettings: SmartGanttSettings) { 41 | const vaultName = this.thisPlugin.app.vault.getName() 42 | localStorage.setItem(`smart-gantt-settings-${vaultName}`, JSON.stringify(newSettings)) 43 | } 44 | 45 | async addPath(path: string) { 46 | if (this.settings.pathListFilter.indexOf(path) === -1) { 47 | this.settings.pathListFilter.push(path) 48 | await this.saveSettings(this._settings) 49 | 50 | } 51 | } 52 | 53 | async removePath(path: string) { 54 | if (this.settings.pathListFilter.indexOf(path) !== -1) { 55 | this.settings.pathListFilter.remove(path) 56 | await this.saveSettings(this._settings) 57 | 58 | } 59 | } 60 | 61 | async setToAllFiles() { 62 | if (this.settings.pathListFilter.indexOf("AllFiles") === -1) { 63 | this.settings.pathListFilter = [] 64 | this.settings.pathListFilter.push("AllFiles") 65 | await this.saveSettings(this._settings) 66 | } 67 | } 68 | 69 | async setToCurrentFiles() { 70 | if (this.settings.pathListFilter.indexOf("CurrentFile") === -1) { 71 | this.settings.pathListFilter = [] 72 | this.settings.pathListFilter.push("CurrentFile") 73 | } 74 | await this.saveSettings(this._settings) 75 | } 76 | 77 | async clearAllPath(){ 78 | this.settings.pathListFilter = [] 79 | await this.saveSettings(this._settings) 80 | } 81 | 82 | async setTodoShowQ(choice: boolean){ 83 | this.settings.todoShowQ = choice 84 | await this.saveSettings(this._settings) 85 | } 86 | 87 | async setDoneShowQ(choice:boolean){ 88 | this.settings.doneShowQ = choice 89 | await this.saveSettings(this._settings) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.1.3.4) 5 | base64 6 | bigdecimal 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | connection_pool (>= 2.2.5) 9 | drb 10 | i18n (>= 1.6, < 2) 11 | minitest (>= 5.1) 12 | mutex_m 13 | tzinfo (~> 2.0) 14 | addressable (2.8.7) 15 | public_suffix (>= 2.0.2, < 7.0) 16 | async (2.14.2) 17 | console (~> 1.25, >= 1.25.2) 18 | fiber-annotation 19 | io-event (~> 1.6, >= 1.6.5) 20 | async-http (0.69.0) 21 | async (>= 2.10.2) 22 | async-pool (~> 0.7) 23 | io-endpoint (~> 0.11) 24 | io-stream (~> 0.4) 25 | protocol-http (~> 0.26) 26 | protocol-http1 (~> 0.19) 27 | protocol-http2 (~> 0.18) 28 | traces (>= 0.10) 29 | async-http-faraday (0.14.0) 30 | async-http (~> 0.42) 31 | faraday 32 | async-pool (0.7.0) 33 | async (>= 1.25) 34 | base64 (0.2.0) 35 | bigdecimal (3.1.8) 36 | concurrent-ruby (1.3.3) 37 | connection_pool (2.4.1) 38 | console (1.27.0) 39 | fiber-annotation 40 | fiber-local (~> 1.1) 41 | json 42 | drb (2.2.1) 43 | faraday (2.10.0) 44 | faraday-net_http (>= 2.0, < 3.2) 45 | logger 46 | faraday-http-cache (2.5.1) 47 | faraday (>= 0.8) 48 | faraday-net_http (3.1.1) 49 | net-http 50 | fiber-annotation (0.2.0) 51 | fiber-local (1.1.0) 52 | fiber-storage 53 | fiber-storage (0.1.2) 54 | github_changelog_generator (1.16.4) 55 | activesupport 56 | async (>= 1.25.0) 57 | async-http-faraday 58 | faraday-http-cache 59 | multi_json 60 | octokit (~> 4.6) 61 | rainbow (>= 2.2.1) 62 | rake (>= 10.0) 63 | i18n (1.14.5) 64 | concurrent-ruby (~> 1.0) 65 | io-endpoint (0.13.0) 66 | io-event (1.6.5) 67 | io-stream (0.4.0) 68 | json (2.7.2) 69 | logger (1.6.0) 70 | minitest (5.24.1) 71 | multi_json (1.15.0) 72 | mutex_m (0.2.0) 73 | net-http (0.4.1) 74 | uri 75 | octokit (4.25.1) 76 | faraday (>= 1, < 3) 77 | sawyer (~> 0.9) 78 | protocol-hpack (1.4.3) 79 | protocol-http (0.28.1) 80 | protocol-http1 (0.19.1) 81 | protocol-http (~> 0.22) 82 | protocol-http2 (0.18.0) 83 | protocol-hpack (~> 1.4) 84 | protocol-http (~> 0.18) 85 | public_suffix (6.0.1) 86 | rainbow (3.1.1) 87 | rake (13.2.1) 88 | sawyer (0.9.2) 89 | addressable (>= 2.3.5) 90 | faraday (>= 0.17.3, < 3) 91 | traces (0.11.1) 92 | tzinfo (2.0.6) 93 | concurrent-ruby (~> 1.0) 94 | uri (0.13.0) 95 | 96 | PLATFORMS 97 | ruby 98 | x86_64-linux 99 | 100 | DEPENDENCIES 101 | github_changelog_generator (~> 1.16) 102 | 103 | BUNDLED WITH 104 | 2.5.11 105 | -------------------------------------------------------------------------------- /quartz.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {QuartzConfig} from "./quartz/cfg" 3 | // @ts-ignore 4 | import * as Plugin from "./quartz/plugins" 5 | 6 | /** 7 | * Quartz 4.0 Configuration 8 | * 9 | * See https://quartz.jzhao.xyz/configuration for more information. 10 | */ 11 | const config: QuartzConfig = { 12 | configuration: { 13 | pageTitle: "Obsidian Smart Gantt", 14 | enableSPA: true, 15 | enablePopovers: true, 16 | // discussion: { 17 | // provider: "giscus", 18 | // configuration: { 19 | // dataRepo: "nhannht/soulstone-backend", 20 | // dataRepoId: "R_kgDOL6t4uA", 21 | // dataCategory: "General", 22 | // dataCategoryId: "DIC_kwDOL6t4uM4CfnWo", 23 | // } 24 | // }, 25 | analytics: { 26 | // @ts-ignore 27 | provider: null, 28 | }, 29 | locale: "en-US", 30 | baseUrl: "https://obsidian-smart-gantt.pages.dev", 31 | ignorePatterns: ["private", "templates", ".obsidian"], 32 | defaultDateType: "created", 33 | theme: { 34 | fontOrigin: "googleFonts", 35 | cdnCaching: true, 36 | typography: { 37 | header: "Cinzel Decorative", 38 | body: "Lora", 39 | code: "Jetbrains Mono", 40 | }, 41 | colors: { 42 | lightMode: { 43 | light: "#ffffff", 44 | lightgray: "#e5e5e5", 45 | gray: "#b8b8b8", 46 | darkgray: "#4e4e4e", 47 | dark: "#2b2b2b", 48 | secondary: "#ea1717", 49 | tertiary: "#84a59d", 50 | highlight: "rgb(250,231,234)", 51 | }, 52 | darkMode: { 53 | light: "#161618", 54 | lightgray: "#393639", 55 | gray: "#646464", 56 | darkgray: "#d4d4d4", 57 | dark: "#ebebec", 58 | secondary: "#7b97aa", 59 | tertiary: "#84a59d", 60 | highlight: "rgba(143, 159, 169, 0.15)", 61 | }, 62 | }, 63 | }, 64 | }, 65 | plugins: { 66 | transformers: [ 67 | Plugin.FrontMatter(), 68 | Plugin.CreatedModifiedDate({ 69 | priority: ["frontmatter", "filesystem"], 70 | }), 71 | Plugin.Latex({renderEngine: "katex"}), 72 | Plugin.SyntaxHighlighting({ 73 | theme: { 74 | light: "github-light", 75 | dark: "github-dark", 76 | }, 77 | keepBackground: false, 78 | }), 79 | Plugin.ObsidianFlavoredMarkdown({enableInHtmlEmbed: false}), 80 | Plugin.GitHubFlavoredMarkdown(), 81 | Plugin.TableOfContents(), 82 | Plugin.CrawlLinks({markdownLinkResolution: "shortest"}), 83 | Plugin.Description(), 84 | ], 85 | filters: [Plugin.RemoveDrafts()], 86 | emitters: [ 87 | Plugin.AliasRedirects(), 88 | Plugin.ComponentResources(), 89 | Plugin.ContentPage(), 90 | Plugin.FolderPage(), 91 | Plugin.TagPage(), 92 | Plugin.ContentIndex({ 93 | enableSiteMap: true, 94 | enableRSS: true, 95 | }), 96 | Plugin.Assets(), 97 | Plugin.Static(), 98 | Plugin.NotFoundPage(), 99 | ], 100 | }, 101 | } 102 | 103 | export default config 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-smart-gantt", 3 | "version": "0.1.17", 4 | "description": "Intelligently generate Gantt chart from your tasks", 5 | "main": "main.js", 6 | "scripts": { 7 | "css": "tailwindcss -i src/style/src.css -o styles.css --watch", 8 | "esbuild": "node esbuild.config.mjs", 9 | "dev": "concurrently \"yarn run css\" \"yarn run esbuild\" ", 10 | "build": "tailwindcss -i src/style/src.css -o styles.css --minify && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 11 | "version": "node version-bump.mjs && git add manifest.json versions.json", 12 | "gen:sample-vault": "zx script/gen-sample-md.mjs --vault ~/obsidian-smart-gantt-sample/sample --fileN 100 --elN 10" 13 | }, 14 | "keywords": [ 15 | "gantt", 16 | "task", 17 | "obsidian" 18 | ], 19 | "author": "https://github.com/nhannht", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@emotion/react": "^11.14.0", 23 | "@emotion/styled": "^11.14.0", 24 | "@faker-js/faker": "^9.4.0", 25 | "@types/crypto-js": "^4.2.2", 26 | "@types/lodash": "^4.17.0", 27 | "@types/mdast": "^4.0.4", 28 | "@types/node": "^16.11.6", 29 | "@types/react": "^18.2.75", 30 | "@types/react-dom": "^18.2.24", 31 | "@types/unist": "^3.0.2", 32 | "@typescript-eslint/eslint-plugin": "5.29.0", 33 | "@typescript-eslint/parser": "5.29.0", 34 | "builtin-modules": "3.3.0", 35 | "concurrently": "^8.2.2", 36 | "esbuild": "0.17.3", 37 | "moment": "^2.30.1", 38 | "obsidian": "latest", 39 | "tailwindcss-scoped-preflight": "^3.4.10", 40 | "tslib": "2.4.0", 41 | "typescript": "4.7.4", 42 | "zx": "^8.1.4" 43 | }, 44 | "dependencies": { 45 | "@codemirror/state": "^6.4.1", 46 | "@codemirror/view": "^6.29.0", 47 | "@radix-ui/react-checkbox": "^1.1.1", 48 | "@radix-ui/react-label": "^2.1.0", 49 | "@radix-ui/react-navigation-menu": "^1.2.0", 50 | "@radix-ui/react-radio-group": "^1.2.0", 51 | "@radix-ui/react-scroll-area": "^1.1.0", 52 | "@radix-ui/react-select": "^2.1.1", 53 | "@radix-ui/react-separator": "^1.1.0", 54 | "@radix-ui/react-slot": "^1.1.0", 55 | "@radix-ui/react-switch": "^1.1.0", 56 | "@radix-ui/react-tooltip": "^1.1.2", 57 | "@wamra/gantt-task-react": "^0.6.17", 58 | "autoprefixer": "^10.4.19", 59 | "chrono-node": "^2.7.5", 60 | "class-variance-authority": "^0.7.0", 61 | "clsx": "^2.1.1", 62 | "crypto-js": "^4.2.0", 63 | "gantt-task-react": "^0.3.9", 64 | "lodash": "^4.17.21", 65 | "lucide-react": "^0.414.0", 66 | "marked": "^13.0.2", 67 | "mdast-util-from-markdown": "^2.0.1", 68 | "micromark": "^4.0.0", 69 | "postcss": "^8.4.38", 70 | "react": "^18.2.0", 71 | "react-dom": "^18.2.0", 72 | "react-icons": "^5.2.1", 73 | "react-resizable-panels": "^2.0.22", 74 | "react-use": "^17.5.0", 75 | "remark": "^15.0.1", 76 | "remark-gfm": "^4.0.0", 77 | "remark-wiki-link": "^2.0.1", 78 | "tailgrids": "^2.1.0", 79 | "tailwind-merge": "^2.4.0", 80 | "tailwindcss": "^3.4.3", 81 | "tailwindcss-animate": "^1.0.7", 82 | "uuid": "^11.0.5" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/addon/mode/loadmode.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function (mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror"), "cjs"); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], function (CM) { 9 | mod(CM, "amd"); 10 | }); 11 | else // Plain browser env 12 | mod(CodeMirror, "plain"); 13 | })(function (CodeMirror, env) { 14 | if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js"; 15 | 16 | var loading = {}; 17 | 18 | function splitCallback(cont, n) { 19 | var countDown = n; 20 | return function () { 21 | if (--countDown == 0) cont(); 22 | }; 23 | } 24 | 25 | function ensureDeps(mode, cont, options) { 26 | var modeObj = CodeMirror.modes[mode], deps = modeObj && modeObj.dependencies; 27 | if (!deps) return cont(); 28 | var missing = []; 29 | for (var i = 0; i < deps.length; ++i) { 30 | if (!CodeMirror.modes.hasOwnProperty(deps[i])) 31 | missing.push(deps[i]); 32 | } 33 | if (!missing.length) return cont(); 34 | var split = splitCallback(cont, missing.length); 35 | for (var i = 0; i < missing.length; ++i) 36 | CodeMirror.requireMode(missing[i], split, options); 37 | } 38 | 39 | CodeMirror.requireMode = function (mode, cont, options) { 40 | if (typeof mode != "string") mode = mode.name; 41 | if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont, options); 42 | if (loading.hasOwnProperty(mode)) return loading[mode].push(cont); 43 | 44 | var file = options && options.path ? options.path(mode) : CodeMirror.modeURL.replace(/%N/g, mode); 45 | if (options && options.loadMode) { 46 | options.loadMode(file, function () { 47 | ensureDeps(mode, cont, options) 48 | }) 49 | } else if (env == "plain") { 50 | var script = document.createElement("script"); 51 | script.src = file; 52 | var others = document.getElementsByTagName("script")[0]; 53 | var list = loading[mode] = [cont]; 54 | CodeMirror.on(script, "load", function () { 55 | ensureDeps(mode, function () { 56 | for (var i = 0; i < list.length; ++i) list[i](); 57 | }, options); 58 | }); 59 | others.parentNode.insertBefore(script, others); 60 | } else if (env == "cjs") { 61 | require(file); 62 | cont(); 63 | } else if (env == "amd") { 64 | requirejs([file], cont); 65 | } 66 | }; 67 | 68 | CodeMirror.autoLoadMode = function (instance, mode, options) { 69 | if (!CodeMirror.modes.hasOwnProperty(mode)) 70 | CodeMirror.requireMode(mode, function () { 71 | instance.setOption("mode", instance.getOption("mode")); 72 | }, options); 73 | }; 74 | }); 75 | -------------------------------------------------------------------------------- /src/GanttBlockManager.tsx: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../main"; 2 | import {createRoot} from "react-dom/client"; 3 | import {SmartGanttSettings} from "./SettingManager"; 4 | import {SmartGanttBlockReactComponentNg} from "./BlockComponent/SmartGanttBlockReactComponentNg"; 5 | import {StrictMode} from "react"; 6 | import {ViewMode} from "gantt-task-react"; 7 | import {TaskListMdBlock} from "@/BlockComponent/TaskListMdBlock"; 8 | import {AppContext} from "@/lib/AppContext"; 9 | 10 | 11 | 12 | export default class GanttBlockManager { 13 | constructor(public thisPlugin: SmartGanttPlugin) { 14 | } 15 | 16 | async registerGanttBlockNg() { 17 | // this.thisPlugin.registerEvent(this.thisPlugin.app.vault.on('modify',(file) =>{ 18 | // console.log(file) 19 | // })) 20 | 21 | this.thisPlugin.registerMarkdownCodeBlockProcessor("gantt", async (source, el, ctx) => { 22 | const settings: SmartGanttSettings = source.trim() !== "" ? JSON.parse(source) : { 23 | doneShowQ: true, 24 | todoShowQ: true, 25 | pathListFilter: ["CurrentFile"], 26 | viewMode:ViewMode.Day, 27 | leftBarChartDisplayQ:true 28 | } 29 | 30 | let root = el.createEl("div", { 31 | cls: "root" 32 | }) 33 | let reactRoot = createRoot(root) 34 | reactRoot.render( 35 | 36 | 42 | 43 | ) 44 | }) 45 | } 46 | 47 | async registerTaskListBlock() { 48 | this.thisPlugin.registerMarkdownCodeBlockProcessor("gantt-list", async (source, el, _ctx) => { 49 | //@ts-ignore 50 | // console.log(_ctx.getSectionInfo(_ctx.el)) 51 | // console.log(source) 52 | const settings: SmartGanttSettings = source.trim() !== "" ? JSON.parse(source) : { 53 | doneShowQ: true, 54 | todoShowQ: true, 55 | pathListFilter: ["CurrentFile"] 56 | } 57 | // console.log(settings) 58 | // console.log(allSentences) 59 | 60 | let root = el.createEl("div", { 61 | cls: "root" 62 | }) 63 | let reactRoot = createRoot(root) 64 | reactRoot.render( 65 | 66 | 67 | 72 | 73 | 74 | 75 | ) 76 | }) 77 | 78 | } 79 | 80 | // async registerGanttBlock() { 81 | // this.thisPlugin.registerMarkdownCodeBlockProcessor("gantt", async (source, el, _ctx) => { 82 | // //@ts-ignore 83 | // // console.log(_ctx.getSectionInfo(_ctx.el)) 84 | // // console.log(source) 85 | // const settings: SmartGanttSettings = source.trim() !== "" ? JSON.parse(source) : { 86 | // doneShowQ: true, 87 | // todoShowQ: true, 88 | // pathListFilter: ["AllFiles"] 89 | // } 90 | // // console.log(settings) 91 | // // console.log(allSentences) 92 | // 93 | // let root = el.createEl("div", { 94 | // cls: "root" 95 | // }) 96 | // let reactRoot = createRoot(root) 97 | // reactRoot.render( 98 | // 99 | // 106 | // ) 107 | // }) 108 | // 109 | // } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/lib/Helper.ts: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../../main"; 2 | import {EditorPosition, MarkdownPostProcessorContext, MarkdownView, WorkspaceLeaf} from "obsidian"; 3 | import {FilterModal} from "@/FilterModal"; 4 | import {Task} from "gantt-task-react"; 5 | import {TimelineExtractorResultNg} from "@/TimelineExtractor"; 6 | import {Node} from "mdast" 7 | import {SmartGanttSettings} from "@/SettingManager"; 8 | export class Helper { 9 | constructor(private thisPlugin: SmartGanttPlugin) { 10 | } 11 | 12 | async reloadView() { 13 | this.thisPlugin.app.workspace.detachLeavesOfType("smart-gantt") 14 | let leaf = this.thisPlugin.app.workspace.getRightLeaf(false); 15 | 16 | leaf?.setViewState({ 17 | type: "smart-gantt", 18 | active: true, 19 | }) 20 | if (leaf instanceof WorkspaceLeaf && !this.thisPlugin.app.workspace.rightSplit.collapsed) { 21 | this.thisPlugin.app.workspace.revealLeaf(leaf); 22 | 23 | } 24 | } 25 | 26 | async renderFilterBox() { 27 | new FilterModal(this.thisPlugin.app, 28 | this.thisPlugin, 29 | ).open() 30 | 31 | } 32 | 33 | getComputedStyleOfVault() { 34 | return getComputedStyle(document.body) 35 | 36 | } 37 | 38 | getAllParentPath = () => { 39 | let allParentPath: Set = new Set() 40 | this.thisPlugin.app.vault.getMarkdownFiles().forEach(r => { 41 | r.parent?.path ? allParentPath.add(r.parent.path) : null 42 | }) 43 | return Array.from(allParentPath) 44 | } 45 | 46 | jumpToPositionOfResult = async (result:TimelineExtractorResultNg)=>{ 47 | const leaf = this.thisPlugin.app.workspace.getLeaf(true) 48 | await leaf.openFile(result.file) 49 | const view = leaf.view as MarkdownView 50 | const node:Node = result.node 51 | // console.log(node) 52 | 53 | view.editor.setSelection({ 54 | line: Number(node.position?.start.line) - 1, 55 | ch: Number(node.position?.start.column) - 1, 56 | } as EditorPosition, 57 | { 58 | line:Number(node.position?.end.line) - 1, 59 | ch: Number(node.position?.end.column) - 1 60 | } as EditorPosition) 61 | 62 | } 63 | 64 | jumpToPositionOfNode= async (task:Task,results:TimelineExtractorResultNg[])=>{ 65 | const result = results.find(r => r.id === task.id) as TimelineExtractorResultNg 66 | const leaf = this.thisPlugin.app.workspace.getLeaf(true) 67 | await leaf.openFile(result.file) 68 | const view = leaf.view as MarkdownView 69 | const node:Node = result.node 70 | // console.log(node) 71 | 72 | view.editor.setSelection({ 73 | line: Number(node.position?.start.line) - 1, 74 | ch: Number(node.position?.start.column) - 1, 75 | } as EditorPosition, 76 | { 77 | line:Number(node.position?.end.line) - 1, 78 | ch: Number(node.position?.end.column) - 1 79 | } as EditorPosition) 80 | } 81 | 82 | updateBlockSettingWithInternalSetting = (settingObject: SmartGanttSettings, 83 | context: MarkdownPostProcessorContext) => { 84 | 85 | const sourcePath = context.sourcePath 86 | //@ts-ignore 87 | const elInfo = context.getSectionInfo(context.el) 88 | // console.log(elInfo) 89 | if (elInfo) { 90 | // console.log(elInfo.text) 91 | let linesFromFile = elInfo.text.split(/(.*?\n)/g) 92 | linesFromFile.forEach((e, i) => { 93 | if (e === "") linesFromFile.splice(i, 1) 94 | }) 95 | // console.log(linesFromFile) 96 | linesFromFile.splice(elInfo.lineStart + 1, 97 | elInfo.lineEnd - elInfo.lineStart - 1, 98 | JSON.stringify(settingObject, null, "\t"), "\n") 99 | // console.log(linesFromFile) 100 | const newSettingsString = linesFromFile.join("") 101 | const file = this.thisPlugin.app.vault.getFileByPath(sourcePath) 102 | if (file) { 103 | this.thisPlugin.app.vault.modify(file, newSettingsString) 104 | } 105 | } 106 | 107 | } 108 | 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/TimelineExtractor.ts: -------------------------------------------------------------------------------- 1 | import {Chrono, ParsedResult} from "chrono-node"; 2 | 3 | import {NodeFromParseTree, TokenWithFile} from "./MarkdownProcesser"; 4 | import {Token} from "marked"; 5 | import {TFile} from "obsidian"; 6 | import {Node} from "unist" 7 | 8 | 9 | export type SmartGanttParsedResults = { 10 | parsedResults: ParsedResult[]|null, 11 | rawText:string 12 | 13 | } 14 | export type TimelineExtractorResult = { 15 | token:Token, 16 | file:TFile, 17 | parsedResultsAndRawText: SmartGanttParsedResults, 18 | 19 | } 20 | 21 | export type TimelineExtractorResultNg = { 22 | id: string, 23 | node:Node, 24 | file:TFile, 25 | parsedResult: ParsedResult|null 26 | 27 | } 28 | 29 | export default class TimelineExtractor { 30 | get countResultWithChrono(): number { 31 | return this.#countResultWithChrono; 32 | } 33 | 34 | get customChrono(): Chrono { 35 | return this._customChrono; 36 | } 37 | 38 | 39 | private readonly _customChrono: Chrono; 40 | 41 | #countResultWithChrono = 0 42 | 43 | constructor(customChrono: Chrono) { 44 | this._customChrono = customChrono; 45 | } 46 | 47 | 48 | private makeTextCompatibleWithTaskPlugin(text: string) { 49 | const hourGlass = text.replace(/⏳/g, " due in "), 50 | airPlain = hourGlass.replace(/🛫/g, " start from "), 51 | heavyPlus = airPlain.replace(/➕/g, " created in "), 52 | checkMark = heavyPlus.replace(/✅/g, " done in "), 53 | crossMark = checkMark.replace(/❌/g, " cancelled in "), 54 | 55 | createdIn = crossMark.replace(/\[created::\s+(.*)]/g, " created in $1 "), 56 | scheduledIn = createdIn.replace(/\[scheduled::\s+(.*)]/g, " scheduled in $1 "), 57 | startFrom = scheduledIn.replace(/\[start::\s+(.*)]/g, " start from $1 "), 58 | dueTo = startFrom.replace(/\[due::\s+(.*)]/g, " due to $1 "), 59 | completionIn = dueTo.replace(/\[completion::\s+(.*)]/g, " completion in $1 "), 60 | cancelledIn = completionIn.replace(/\[cancelled::\s(.*)]/g, " cancelled in $1 "), 61 | 62 | calendarMark = cancelledIn.replace("/📅/g", " to ") 63 | 64 | return calendarMark 65 | 66 | } 67 | 68 | 69 | async GetTimelineDataFromNodes(nodes:NodeFromParseTree[]):Promise { 70 | let results:TimelineExtractorResultNg[] = [] 71 | nodes.forEach(((node,nodeId)=>{ 72 | //@ts-ignore 73 | let rawText = node.node.children[0].children[0].value 74 | let transformedText = this.makeTextCompatibleWithTaskPlugin(rawText) 75 | const parsedResults = this.customChrono.parse(transformedText) 76 | if (parsedResults && parsedResults.length > 0 ){ 77 | parsedResults.forEach((r,rId) =>{ 78 | results.push({ 79 | id: `${nodeId}-${rId}`, 80 | node:node.node, 81 | file:node.file, 82 | parsedResult:r 83 | }) 84 | 85 | }) 86 | } else { 87 | results.push({ 88 | id: `${nodeId}`, 89 | node:node.node, 90 | file:node.file, 91 | parsedResult:null 92 | }) 93 | } 94 | })) 95 | return results 96 | 97 | } 98 | 99 | 100 | async GetTimelineDataFromDocumentArrayWithChrono(tokens: TokenWithFile[] | null, 101 | ): Promise { 102 | // let timelineData: TimelineEntryChrono[] = [] 103 | let extractorResultList: TimelineExtractorResult[] = [] 104 | // let documents: Document[] = [] 105 | // console.log(tokens) 106 | tokens?.forEach((token) => { 107 | let parsedResult:ParsedResult[] = [] 108 | if ("text" in token.token) { 109 | const taskPluginCompatibleText = this.makeTextCompatibleWithTaskPlugin(token.token.text) 110 | parsedResult = this.customChrono.parse(taskPluginCompatibleText) 111 | } 112 | if (parsedResult && parsedResult.length > 0) { 113 | this.#countResultWithChrono = this.#countResultWithChrono + 1 114 | const smartGanttParsedResults:SmartGanttParsedResults = { 115 | parsedResults: parsedResult, 116 | //@ts-ignore 117 | rawText: token.token.text 118 | } 119 | extractorResultList.push({ 120 | ...token, 121 | parsedResultsAndRawText: smartGanttParsedResults 122 | }) 123 | } else if (parsedResult.length === 0){ 124 | const smartGanttParsedResults: SmartGanttParsedResults = { 125 | parsedResults:null, 126 | //@ts-ignore 127 | rawText:token.token.text 128 | } 129 | extractorResultList.push({ 130 | ...token, 131 | parsedResultsAndRawText: smartGanttParsedResults, 132 | }) 133 | } 134 | }) 135 | 136 | return extractorResultList 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/BlockComponent/SmartGanttBlockReactComponentNg.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from "react"; 2 | import {MarkdownPostProcessorContext} from "obsidian"; 3 | import SmartGanttPlugin from "../../main"; 4 | import {SmartGanttSettings} from "@/SettingManager"; 5 | import MarkdownProcesser from "../MarkdownProcesser"; 6 | import TimelineExtractor, {TimelineExtractorResultNg} from "../TimelineExtractor"; 7 | import {Chrono, ParsedComponents} from "chrono-node"; 8 | import {Task} from 'gantt-task-react'; 9 | import SettingViewComponent from "../component/SettingViewComponent"; 10 | import SmartGanttChart from "../component/SmartGanttChart"; 11 | import {ListItem} from "mdast"; 12 | import {NavBar} from "@/BlockComponent/NavBar"; 13 | 14 | export const SmartGanttBlockReactComponentNg = (props: { 15 | ctx: MarkdownPostProcessorContext, 16 | src: string, 17 | thisPlugin: SmartGanttPlugin, 18 | settings: SmartGanttSettings 19 | }) => { 20 | const [internalSettings, setInternalSettings] = 21 | useState(structuredClone(props.settings)) 22 | const [isSettingQ, setIsSettingQ] = useState(false) 23 | // const [resultWithChronoCount, setResultWithChronoCount] = useState(0) 24 | const [timelineResults, setTimelineResults] = useState([]) 25 | const [tasks, setTasks] = useState([]) 26 | 27 | 28 | const reupdateData = useCallback(async () => { 29 | const allMarkdownFiles = props.thisPlugin.app.vault.getMarkdownFiles(); 30 | const markdownProcesser = new MarkdownProcesser(allMarkdownFiles, props.thisPlugin) 31 | await markdownProcesser.parseAllFilesNg(internalSettings) 32 | const allNodes = markdownProcesser.nodes 33 | // console.log(allNodes) 34 | const timelineExtractor = new TimelineExtractor(new Chrono()) 35 | const timelineExtractorResults = await timelineExtractor.GetTimelineDataFromNodes(allNodes) 36 | // console.log(timelineExtractorResults) 37 | setTimelineResults(timelineExtractorResults) 38 | // console.log(tasks) 39 | }, [internalSettings]) 40 | 41 | const createDateFromKnownValues = useCallback((p: ParsedComponents) => { 42 | //@ts-ignore 43 | const knownValues = p.knownValues 44 | return new Date(knownValues.year, knownValues.month, knownValues.day) 45 | }, [] 46 | ) 47 | 48 | useEffect(() => { 49 | reupdateData().then(_r => null) 50 | }, [internalSettings]); 51 | 52 | useEffect(() => { 53 | let tempTasks: Task[] = [] 54 | timelineResults.forEach((timelineResult, _tIndex) => { 55 | if (timelineResult.parsedResult) { 56 | // console.log(timelineResult.parsedResult.start) 57 | const startComponent = timelineResult.parsedResult.start 58 | const endComponent = timelineResult.parsedResult.end 59 | let task: Task = { 60 | start: createDateFromKnownValues(startComponent), 61 | end: endComponent ? createDateFromKnownValues(endComponent) : createDateFromKnownValues(startComponent), 62 | //@ts-ignore 63 | name: timelineResult.node.children[0].children[0].value, 64 | id: `${timelineResult.id}`, 65 | type: 'task', 66 | progress: 50, 67 | isDisabled: true, 68 | styles: (timelineResult.node as ListItem).checked ? { 69 | progressColor: '#df1fc0', 70 | progressSelectedColor: '#20f323' 71 | } : { 72 | progressColor: '#ffffff', 73 | progressSelectedColor: '#000000' 74 | }, 75 | } 76 | // console.log(task) 77 | // console.log(task) 78 | tempTasks.push(task) 79 | } 80 | }) 81 | setTasks(tempTasks) 82 | }, [timelineResults]) 83 | 84 | let mainComponent: JSX.Element 85 | 86 | 87 | if (isSettingQ) { 88 | mainComponent =
89 | { 92 | setIsSettingQ(is) 93 | }} 94 | inputS={internalSettings} 95 | saveSettings={(s) => { 96 | setInternalSettings(s) 97 | 98 | }} 99 | updateSettingInCodeBlockHandle={(s) => { 100 | props.thisPlugin.helper.updateBlockSettingWithInternalSetting(s, props.ctx) 101 | }} 102 | thisPlugin={props.thisPlugin} 103 | /> 104 |
105 | } else { 106 | if (tasks.length > 0) { 107 | mainComponent =
108 |
109 | 111 |
112 | 118 | {/**/} 122 |
123 | } else { 124 | mainComponent =
125 |
126 | 128 |
129 |
130 | } 131 | } 132 | 133 | return <> 134 | {mainComponent} 135 | 136 | }; 137 | -------------------------------------------------------------------------------- /src/BlockComponent/TaskListMdBlock.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from "react"; 2 | import {MarkdownPostProcessorContext} from "obsidian"; 3 | import SmartGanttPlugin from "../../main"; 4 | import {SmartGanttSettings} from "@/SettingManager"; 5 | import MarkdownProcesser from "../MarkdownProcesser"; 6 | import TimelineExtractor, {TimelineExtractorResultNg} from "../TimelineExtractor"; 7 | import {Chrono, ParsedComponents} from "chrono-node"; 8 | import {Task} from 'gantt-task-react'; 9 | import SettingViewComponent from "../component/SettingViewComponent"; 10 | import TaskList from "../component/TaskList"; 11 | 12 | import {NavBar} from "@/BlockComponent/NavBar"; 13 | 14 | 15 | export const TaskListMdBlock = (props: { 16 | ctx: MarkdownPostProcessorContext, 17 | src: string, 18 | thisPlugin: SmartGanttPlugin, 19 | settings: SmartGanttSettings 20 | }) => { 21 | const [internalSettings, setInternalSettings] = 22 | useState(structuredClone(props.settings)) 23 | const [isSettingQ, setIsSettingQ] = useState(false) 24 | // const [resultWithChronoCount, setResultWithChronoCount] = useState(0) 25 | const [timelineResults, setTimelineResults] = useState([]) 26 | const [tasks, setTasks] = useState([]) 27 | 28 | 29 | 30 | 31 | // useMemo(() => { 32 | // props.thisPlugin.registerEvent(app.vault.on('modify', consoleMyName)) 33 | // // console.log("Memo memo") 34 | // }, []) 35 | 36 | const reupdateData = useCallback(async () => { 37 | const allMarkdownFiles = props.thisPlugin.app.vault.getMarkdownFiles(); 38 | const markdownProcesser = new MarkdownProcesser(allMarkdownFiles, props.thisPlugin) 39 | await markdownProcesser.parseAllFilesNg(internalSettings) 40 | const allNodes = markdownProcesser.nodes 41 | // console.log(allNodes) 42 | const timelineExtractor = new TimelineExtractor(new Chrono()) 43 | const timelineExtractorResults = await timelineExtractor.GetTimelineDataFromNodes(allNodes) 44 | // console.log(timelineExtractorResults) 45 | setTimelineResults(timelineExtractorResults) 46 | // console.log(tasks) 47 | }, [internalSettings]) 48 | 49 | const createDateFromKnownValues = useCallback((p: ParsedComponents) => { 50 | //@ts-ignore 51 | const knownValues = p.knownValues 52 | return new Date(knownValues.year, knownValues.month, knownValues.day) 53 | }, [] 54 | ) 55 | 56 | useEffect(() => { 57 | reupdateData().then(_r => null) 58 | }, [internalSettings]); 59 | 60 | useEffect(() => { 61 | let tempTasks: Task[] = [] 62 | timelineResults.forEach((timelineResult, tIndex) => { 63 | if (timelineResult.parsedResult) { 64 | // console.log(timelineResult.parsedResult.start) 65 | const startComponent = timelineResult.parsedResult.start 66 | const endComponent = timelineResult.parsedResult.end 67 | let task: Task = { 68 | start: createDateFromKnownValues(startComponent), 69 | end: endComponent ? createDateFromKnownValues(endComponent) : createDateFromKnownValues(startComponent), 70 | //@ts-ignore 71 | name: timelineResult.node.children[0].children[0].value, 72 | id: `${tIndex}`, 73 | type: 'task', 74 | progress: 50, 75 | isDisabled: true, 76 | styles: {progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d'}, 77 | } 78 | // console.log(task) 79 | // console.log(task) 80 | tempTasks.push(task) 81 | } 82 | }) 83 | setTasks(tempTasks) 84 | }, [timelineResults]) 85 | 86 | let mainComponent = <> 87 | 88 | 89 | const modifyResultsStatus = useCallback((resultId: string, status: boolean) => { 90 | let resultsClone = [...timelineResults] 91 | // console.log(resultsClone) 92 | // console.log(timelineResults) 93 | let resultFind = resultsClone.find(r => r.id === resultId) 94 | // console.log(resultId) 95 | // console.log(resultFind) 96 | if (resultFind) { 97 | //@ts-ignore 98 | resultFind.node.checked = status 99 | setTimelineResults(resultsClone) 100 | } 101 | 102 | }, [timelineResults]) 103 | 104 | 105 | if (isSettingQ) { 106 | mainComponent =
107 | { 110 | setIsSettingQ(is) 111 | }} 112 | inputS={internalSettings} 113 | saveSettings={(s) => { 114 | setInternalSettings(s) 115 | 116 | }} 117 | updateSettingInCodeBlockHandle={(s) => { 118 | props.thisPlugin.helper.updateBlockSettingWithInternalSetting(s, props.ctx) 119 | }} 120 | thisPlugin={props.thisPlugin} 121 | /> 122 |
123 | } else { 124 | if (tasks.length > 0) { 125 | mainComponent =
127 | 128 | {/**/} 133 | 137 |
138 |
139 | } else { 140 | mainComponent =
setIsSettingQ(true)}> 142 | 143 |
144 | } 145 | } 146 | 147 | return <> 148 | {mainComponent} 149 | 150 | }; 151 | -------------------------------------------------------------------------------- /src/component/NavMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" 3 | import { cva } from "class-variance-authority" 4 | import { ChevronDown } from "lucide-react" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const NavigationMenu = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 20 | {children} 21 | 22 | 23 | )) 24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName 25 | 26 | const NavigationMenuList = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, ...props }, ref) => ( 30 | 38 | )) 39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName 40 | 41 | const NavigationMenuItem = NavigationMenuPrimitive.Item 42 | 43 | const navigationMenuTriggerStyle = cva( 44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" 45 | ) 46 | 47 | const NavigationMenuTrigger = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, children, ...props }, ref) => ( 51 | 56 | {children}{" "} 57 | 62 | )) 63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName 64 | 65 | const NavigationMenuContent = React.forwardRef< 66 | React.ElementRef, 67 | React.ComponentPropsWithoutRef 68 | >(({ className, ...props }, ref) => ( 69 | 77 | )) 78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName 79 | 80 | const NavigationMenuLink = NavigationMenuPrimitive.Link 81 | 82 | const NavigationMenuViewport = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 |
87 | 95 |
96 | )) 97 | NavigationMenuViewport.displayName = 98 | NavigationMenuPrimitive.Viewport.displayName 99 | 100 | const NavigationMenuIndicator = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 112 |
113 | 114 | )) 115 | NavigationMenuIndicator.displayName = 116 | NavigationMenuPrimitive.Indicator.displayName 117 | 118 | export { 119 | navigationMenuTriggerStyle, 120 | NavigationMenu, 121 | NavigationMenuList, 122 | NavigationMenuItem, 123 | NavigationMenuContent, 124 | NavigationMenuTrigger, 125 | NavigationMenuLink, 126 | NavigationMenuIndicator, 127 | NavigationMenuViewport, 128 | } 129 | -------------------------------------------------------------------------------- /main.tsx: -------------------------------------------------------------------------------- 1 | import {Plugin, WorkspaceLeaf} from 'obsidian'; 2 | import SmartGanttSibeBarView from "@/sidebar/SmartGanttSibeBarView"; 3 | import {Helper} from "@/lib/Helper"; 4 | import SettingManager, {SmartGanttSettings} from "./src/SettingManager"; 5 | import GanttBlockManager from "./src/GanttBlockManager"; 6 | import {ViewMode} from "gantt-task-react"; 7 | import './src/lib/codemirror'; 8 | import './src/mode/gantt/gantt' 9 | import './src/mode/gantt/gantt-list' 10 | import SmartGanttItemView, {SMART_GANTT_ITEM_VIEW_TYPE} from "@/GanttItemView"; 11 | 12 | const DEFAULT_SETTINGS: SmartGanttSettings = { 13 | pathListFilter: ["AllFiles"], 14 | todoShowQ: true, 15 | doneShowQ: true, 16 | leftBarChartDisplayQ: true, 17 | viewMode: ViewMode.Day 18 | 19 | } 20 | 21 | 22 | export default class SmartGanttPlugin extends Plugin { 23 | settingManager = new SettingManager(this, DEFAULT_SETTINGS); 24 | public helper = new Helper(this) 25 | ganttBlockManager = new GanttBlockManager(this) 26 | modesToKeep = ["hypermd", "markdown", "null", "xml"]; 27 | 28 | 29 | refreshLeaves = () => { 30 | // re-set the editor mode to refresh the syntax highlighting 31 | //@ts-ignore 32 | this.app.workspace.iterateCodeMirrors(cm => cm.setOption("mode", cm.getOption("mode"))) 33 | } 34 | 35 | darkModeAdapt = () => { 36 | if (document.body.hasClass("theme-dark")) { 37 | document.body.addClass("dark") 38 | } else { 39 | document.body.removeClass("dark") 40 | } 41 | } 42 | 43 | 44 | override async onload() { 45 | this.darkModeAdapt() 46 | 47 | await this.settingManager.loadSettings() 48 | this.registerView(SMART_GANTT_ITEM_VIEW_TYPE,(leaf)=> new SmartGanttItemView(leaf, this)) 49 | this.registerExtensions(["smartgantt"],SMART_GANTT_ITEM_VIEW_TYPE) 50 | 51 | 52 | this.addRibbonIcon("shell", "Debug, open Gantt view",async ()=>{ 53 | // let leaf = this.app.workspace.getLeaf(false) 54 | // leaf.setViewState({ 55 | // type:SMART_GANTT_ITEM_VIEW_TYPE, 56 | // active:true, 57 | // state:{ 58 | // projectId: "default", 59 | // projectName: "default" 60 | // }as SmartGanttItemViewState 61 | // }) 62 | // 63 | const currentFile = this.app.workspace.getActiveFile() 64 | if (currentFile){ 65 | // console.log(currentFile) 66 | // const helper = new HelperNg(this) 67 | // const tree = await helper.getParseTree(currentFile) 68 | // console.log(tree) 69 | const view = this.app.workspace.getActiveViewOfType(SmartGanttItemView) 70 | if (view){ 71 | console.log(view.getState()) 72 | } 73 | 74 | } 75 | }) 76 | 77 | this.app.workspace.onLayoutReady(() => { 78 | this.refreshLeaves() 79 | 80 | }) 81 | 82 | // console.log(this.settingManager.settings) 83 | this.addCommand({ 84 | id: 'smart-gantt-reload', 85 | name: 'Reload', 86 | callback: () => { 87 | this.helper.reloadView() 88 | } 89 | }) 90 | 91 | 92 | this.registerView("smart-gantt", (leaf) => { 93 | return new SmartGanttSibeBarView(leaf, this); 94 | }) 95 | 96 | this.addRibbonIcon('egg', 'Smart Gantt', () => { 97 | 98 | let leafs = this.app.workspace.getLeavesOfType("smart-gantt"); 99 | if (leafs.length > 0) { 100 | // this.app.workspace.detachLeavesOfType("smart-gantt") 101 | let leaf = leafs[0]; 102 | // console.log(leaf.getEphemeralState()) 103 | if (this.app.workspace.rightSplit.collapsed) { 104 | this.app.workspace.revealLeaf(leaf) 105 | } else { 106 | this.app.workspace.rightSplit.collapse() 107 | } 108 | } else { 109 | let leaf = this.app.workspace.getRightLeaf(false); 110 | 111 | leaf?.setViewState({ 112 | type: "smart-gantt", 113 | active: true 114 | }) 115 | if (leaf instanceof WorkspaceLeaf) { 116 | this.app.workspace.revealLeaf(leaf); 117 | } 118 | } 119 | 120 | }) 121 | 122 | // await this.ganttBlockManager.registerGanttBlock() 123 | await this.ganttBlockManager.registerGanttBlockNg() 124 | await this.ganttBlockManager.registerTaskListBlock() 125 | 126 | 127 | this.registerEvent(this.app.workspace.on('css-change', this.darkModeAdapt)) 128 | 129 | 130 | // This adds a settings tab so the user can configure various aspects of the plugin 131 | // this.addSettingTab(new SampleSettingTab(this.app, this)); 132 | 133 | } 134 | 135 | override async onunload() { 136 | // this.app.workspace.detachLeavesOfType("gantt-chart") 137 | await this.settingManager.saveSettings(this.settingManager.settings) 138 | //@ts-ignore 139 | for (const key in CodeMirror.modes) { 140 | // @ts-ignore 141 | if (CodeMirror.modes.hasOwnProperty(key) && !this.modesToKeep.includes(key)) { 142 | // @ts-ignore 143 | delete CodeMirror.modes[key]; 144 | } 145 | this.refreshLeaves() 146 | 147 | } 148 | this.app.workspace.off('css-change', this.darkModeAdapt) 149 | 150 | } 151 | 152 | 153 | } 154 | 155 | 156 | // class SampleSettingTab extends PluginSettingTab { 157 | // plugin: SmartGanttPlugin; 158 | // 159 | // constructor(app: App, plugin: SmartGanttPlugin) { 160 | // super(app, plugin); 161 | // this.plugin = plugin; 162 | // } 163 | // 164 | // display(): void { 165 | // const {containerEl} = this; 166 | // 167 | // containerEl.empty(); 168 | // 169 | // new Setting(containerEl) 170 | // .setName('Setting #1') 171 | // .setDesc('It\'s a secret') 172 | // .addText(text => text 173 | // .setPlaceholder('Enter your secret') 174 | // .setValue(this.plugin.settingManager.settings.mySetting) 175 | // .onChange(async (value) => { 176 | // this.plugin.settingManager.settings.mySetting = value; 177 | // await this.plugin.settingManager.saveSettings(this.plugin.settingManager.settings) 178 | // })); 179 | // } 180 | // } 181 | -------------------------------------------------------------------------------- /src/component/TaskList.tsx: -------------------------------------------------------------------------------- 1 | import {TimelineExtractorResultNg} from "@/TimelineExtractor"; 2 | import SmartGanttPlugin from "../../main"; 3 | import {useCallback, useEffect, useState} from "react"; 4 | import {ListItem} from "mdast"; 5 | import {ScrollArea} from "./ScrollableList"; 6 | import {Checkbox} from "./Checkbox"; 7 | import {Label} from "@/component/Label"; 8 | import {Separator} from "@/component/Separator"; 9 | import {ResizableHandle, ResizablePanel, ResizablePanelGroup} from "@/component/ResizablePanel"; 10 | 11 | const TodoList = (props: { 12 | todos: TimelineExtractorResultNg[], 13 | modifyCheckboxFn: (t: TimelineExtractorResultNg, status: boolean) => void 14 | changeResultStatusFn: (rId: string, status: boolean) => void 15 | jumpToResultPositionFn: (t: TimelineExtractorResultNg) => void 16 | }) => { 17 | 18 | 19 | return 20 |
Unchecked 22 |
23 | {props.todos.map((t, id) => ( 24 |
25 |
{ 28 | props.modifyCheckboxFn(t, Boolean(e)) 29 | //@ts-ignore 30 | props.changeResultStatusFn(t.id, Boolean(e)) 31 | }} 32 | checked={Boolean((props.todos.find(e => e.id === t.id)?.node as ListItem).checked)} 33 | /> 34 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | ))} 52 |
53 | } 54 | 55 | const DoneList = (props: { 56 | dones: TimelineExtractorResultNg[], 57 | modifyCheckboxFn: (t: TimelineExtractorResultNg, status: boolean) => void, 58 | changeResultStatusFn: (rId: string, status: boolean) => void 59 | jumpToResultPositionFn: (t: TimelineExtractorResultNg) => void; 60 | }) => { 61 | return 62 |
Checked 64 |
65 | {props.dones.map((d, id) => ( 66 |
67 |
{ 70 | props.modifyCheckboxFn(d, Boolean(e)) 71 | props.changeResultStatusFn(d.id, Boolean(e)) 72 | 73 | }} 74 | checked={Boolean((props.dones.find(e => e.id === d.id)?.node as ListItem).checked)} 75 | /> 76 |
86 | 87 |
88 | 89 | ))} 90 |
91 | } 92 | const TaskList = (props: { 93 | results: TimelineExtractorResultNg[] 94 | thisPlugin: SmartGanttPlugin, 95 | changeResultStatusFn: (rId: string, status: boolean) => void, 96 | }) => { 97 | const [todos, setTodos] = useState([]) 98 | const [dones, setDones] = useState([]) 99 | 100 | const classifyResults = useCallback(() => { 101 | let ts: TimelineExtractorResultNg[] = [] 102 | let ds: TimelineExtractorResultNg[] = [] 103 | props.results.forEach((t) => { 104 | const node = t.node as ListItem 105 | if (node.checked) { 106 | ds.push(t) 107 | } else { 108 | ts.push(t) 109 | } 110 | }) 111 | setTodos(ts) 112 | setDones(ds) 113 | }, [props.results]) 114 | 115 | useEffect(() => { 116 | classifyResults() 117 | }, [props.results]) 118 | 119 | const modifyCheckboxFn = useCallback(async (r: TimelineExtractorResultNg, checkedQ: boolean) => { 120 | let fileContent = await props.thisPlugin.app.vault.read(r.file) 121 | let lines = fileContent.split(/(.*?\n)/g) 122 | lines.forEach(((l, i) => { 123 | if (l.trim() === "") lines.splice(i, 1) 124 | })) 125 | // console.log(lines) 126 | let lI = Number(r.node.position?.start.line) - 1 127 | // console.log(lines[lI]) 128 | let newLine = "" 129 | if (checkedQ) { 130 | newLine = lines[lI].replace("[ ]", "[x]") 131 | } else { 132 | newLine = lines[lI].replace("[x]", "[ ]") 133 | } 134 | // console.log(newLine) 135 | // console.log(lI) 136 | lines.splice(lI, 1, newLine) 137 | const newFileContent = lines.join("") 138 | await props.thisPlugin.app.vault.modify(r.file, newFileContent) 139 | }, [] 140 | ) 141 | 142 | // useEffect(() => { 143 | // console.log(todos) 144 | // }, [todos]); 145 | 146 | 147 | return
148 | 152 | 153 | 158 | 159 | 160 | 161 | 166 | 167 | 168 | 169 |
170 | } 171 | 172 | export default TaskList 173 | -------------------------------------------------------------------------------- /src/component/Select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /src/sidebar/SidebarReactComponentNg.tsx: -------------------------------------------------------------------------------- 1 | import SmartGanttPlugin from "../../main"; 2 | import {useLocalStorage} from "react-use"; 3 | import {SmartGanttSettings} from "@/SettingManager"; 4 | import {useCallback, useEffect, useState} from "react"; 5 | import MarkdownProcesser from "../MarkdownProcesser"; 6 | import TimelineExtractor, {TimelineExtractorResultNg} from "../TimelineExtractor"; 7 | import {Chrono, ParsedComponents} from "chrono-node"; 8 | import {Task, ViewMode} from "gantt-task-react"; 9 | import {ListItem} from "mdast"; 10 | import SmartGanttChart from "@/component/SmartGanttChart"; 11 | import SettingViewComponent from "@/component/SettingViewComponent"; 12 | import {ResizableHandle, ResizablePanel, ResizablePanelGroup} from "@/component/ResizablePanel"; 13 | import TaskList from "@/component/TaskList"; 14 | import {NavBar} from "@/BlockComponent/NavBar"; 15 | 16 | const SidebarReactComponentNg = (props: { 17 | thisPlugin: SmartGanttPlugin 18 | }) => { 19 | 20 | 21 | const [settings, 22 | saveSettings 23 | ] = 24 | useLocalStorage 25 | (`smart-gantt-sidebar-settings-${props.thisPlugin.app.vault.getName()}`, 26 | { 27 | doneShowQ: true, 28 | todoShowQ: true, 29 | pathListFilter: ["CurrentFile"], 30 | leftBarChartDisplayQ: false, 31 | viewMode: ViewMode.Day 32 | }) 33 | const [timelineResults, setTimelineResults] = useState([]) 34 | 35 | const [isSettingQ, setIsSettingQ] = useState(false) 36 | const [tasks, setTasks] = useState([]) 37 | const reupdateData = useCallback(async () => { 38 | const allMarkdownFiles = props.thisPlugin.app.vault.getMarkdownFiles(); 39 | const markdownProcesser = new MarkdownProcesser(allMarkdownFiles, props.thisPlugin) 40 | if (!settings) { 41 | console.log("settings is undefined") 42 | saveSettings({ 43 | doneShowQ: true, 44 | todoShowQ: true, 45 | pathListFilter: ["CurrentFile"], 46 | leftBarChartDisplayQ: false, 47 | viewMode: ViewMode.Day 48 | }) 49 | } 50 | //@ts-ignore 51 | await markdownProcesser.parseAllFilesNg(settings) 52 | const allNodes = markdownProcesser.nodes 53 | // console.log(allNodes) 54 | const timelineExtractor = new TimelineExtractor(new Chrono()) 55 | const timelineExtractorResults = await timelineExtractor.GetTimelineDataFromNodes(allNodes) 56 | // console.log(timelineExtractorResults) 57 | setTimelineResults(timelineExtractorResults) 58 | // console.log(tasks) 59 | }, [settings]) 60 | 61 | 62 | const createDateFromKnownValues = useCallback((p: ParsedComponents) => { 63 | //@ts-ignore 64 | const knownValues = p.knownValues 65 | return new Date(knownValues.year, knownValues.month, knownValues.day) 66 | }, [] 67 | ) 68 | 69 | useEffect(() => { 70 | reupdateData().then(_r => null) 71 | }, [settings]) 72 | 73 | useEffect(() => { 74 | let tempTasks: Task[] = [] 75 | timelineResults.forEach((timelineResult, _tIndex) => { 76 | if (timelineResult.parsedResult) { 77 | // console.log(timelineResult.parsedResult.start) 78 | const startComponent = timelineResult.parsedResult.start 79 | const endComponent = timelineResult.parsedResult.end 80 | let task: Task = { 81 | start: createDateFromKnownValues(startComponent), 82 | end: endComponent ? createDateFromKnownValues(endComponent) : createDateFromKnownValues(startComponent), 83 | //@ts-ignore 84 | name: timelineResult.node.children[0].children[0].value, 85 | id: `${timelineResult.id}`, 86 | type: 'task', 87 | progress: 50, 88 | isDisabled: true, 89 | styles: (timelineResult.node as ListItem).checked ? { 90 | progressColor: '#df1fc0', 91 | progressSelectedColor: '#20f323' 92 | } : { 93 | progressColor: '#ffffff', 94 | progressSelectedColor: '#000000' 95 | }, 96 | } 97 | // console.log(task) 98 | tempTasks.push(task) 99 | } 100 | }) 101 | setTasks(tempTasks) 102 | }, [timelineResults]) 103 | const modifyResultsStatus = useCallback((resultId: string, status: boolean) => { 104 | let resultsClone = [...timelineResults] 105 | // console.log(resultsClone) 106 | // console.log(timelineResults) 107 | let resultFind = resultsClone.find(r => r.id === resultId) 108 | // console.log(resultId) 109 | // console.log(resultFind) 110 | if (resultFind) { 111 | //@ts-ignore 112 | resultFind.node.checked = status 113 | setTimelineResults(resultsClone) 114 | } 115 | 116 | }, [timelineResults]) 117 | 118 | 119 | let mainComponent = <> 120 | 121 | 122 | if (isSettingQ) { 123 | mainComponent =
124 | { 127 | setIsSettingQ(is) 128 | }} 129 | inputS={settings} 130 | saveSettings={(s) => { 131 | saveSettings(s) 132 | }} 133 | thisPlugin={props.thisPlugin} 134 | /> 135 |
136 | } else { 137 | if (tasks.length > 0) { 138 | mainComponent =
139 |
140 | 145 |
146 | 150 | 151 | 157 | 158 | 159 | 160 | 163 | 164 | 165 | {/**/} 169 | 170 |
171 | } else { 172 | mainComponent =
173 |
178 |
179 | } 180 | } 181 | 182 | return <> 183 | {mainComponent} 184 | 185 | 186 | 187 | } 188 | 189 | export default SidebarReactComponentNg 190 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.16](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.16) (2024-07-29) 4 | 5 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.14...0.1.16) 6 | 7 | **Closed issues:** 8 | 9 | - make dark mode automatic oh-my-good-it-working [\#27](https://github.com/nhannht/obsidian-smart-gantt/issues/27) 10 | 11 | ## [0.1.14](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.14) (2024-07-28) 12 | 13 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.13...0.1.14) 14 | 15 | **Closed issues:** 16 | 17 | - separate gantt chart component [\#23](https://github.com/nhannht/obsidian-smart-gantt/issues/23) 18 | - add json syntax highlight in the blocks [\#18](https://github.com/nhannht/obsidian-smart-gantt/issues/18) 19 | - Display Syntax error in text [\#8](https://github.com/nhannht/obsidian-smart-gantt/issues/8) 20 | - \[possible bug\]: Maximum text size in diagram exceeded [\#6](https://github.com/nhannht/obsidian-smart-gantt/issues/6) 21 | 22 | ## [0.1.13](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.13) (2024-07-26) 23 | 24 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.12...0.1.13) 25 | 26 | ## [0.1.12](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.12) (2024-07-26) 27 | 28 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.11...0.1.12) 29 | 30 | **Closed issues:** 31 | 32 | - generate fake vault for testing [\#21](https://github.com/nhannht/obsidian-smart-gantt/issues/21) 33 | - feature to change the color of the plot [\#12](https://github.com/nhannht/obsidian-smart-gantt/issues/12) 34 | 35 | ## [0.1.11](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.11) (2024-07-05) 36 | 37 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.10...0.1.11) 38 | 39 | **Closed issues:** 40 | 41 | - convert entire sidebar to react [\#17](https://github.com/nhannht/obsidian-smart-gantt/issues/17) 42 | - showing all parrent path in settings go wrong due to the filter logic move to stay in markdown processing phase [\#16](https://github.com/nhannht/obsidian-smart-gantt/issues/16) 43 | - allow user change config in the block via modal [\#15](https://github.com/nhannht/obsidian-smart-gantt/issues/15) 44 | - bug: Error: \ attribute width: A negative value is not valid. \("-37.5"\) [\#14](https://github.com/nhannht/obsidian-smart-gantt/issues/14) 45 | - Gantt code block stopped working [\#13](https://github.com/nhannht/obsidian-smart-gantt/issues/13) 46 | 47 | ## [0.1.10](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.10) (2024-07-03) 48 | 49 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.9...0.1.10) 50 | 51 | **Closed issues:** 52 | 53 | - How to filter out done tasks? [\#9](https://github.com/nhannht/obsidian-smart-gantt/issues/9) 54 | 55 | ## [0.1.9](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.9) (2024-06-29) 56 | 57 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.8...0.1.9) 58 | 59 | ## [0.1.8](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.8) (2024-06-29) 60 | 61 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.7...0.1.8) 62 | 63 | ## [0.1.7](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.7) (2024-06-28) 64 | 65 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.6...0.1.7) 66 | 67 | ## [0.1.6](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.6) (2024-06-17) 68 | 69 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.5...0.1.6) 70 | 71 | **Closed issues:** 72 | 73 | - Limit relevant tasks to a folder [\#5](https://github.com/nhannht/obsidian-smart-gantt/issues/5) 74 | - Tasks identification problem [\#3](https://github.com/nhannht/obsidian-smart-gantt/issues/3) 75 | 76 | ## [0.1.5](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.5) (2024-06-17) 77 | 78 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.4...0.1.5) 79 | 80 | ## [0.1.4](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.4) (2024-06-16) 81 | 82 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.3...0.1.4) 83 | 84 | **Closed issues:** 85 | 86 | - tw [\#4](https://github.com/nhannht/obsidian-smart-gantt/issues/4) 87 | 88 | ## [0.1.3](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.3) (2024-06-08) 89 | 90 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.2...0.1.3) 91 | 92 | **Closed issues:** 93 | 94 | - mermaid version error \(version 0.0.8\) [\#1](https://github.com/nhannht/obsidian-smart-gantt/issues/1) 95 | 96 | ## [0.1.2](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.2) (2024-05-27) 97 | 98 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.1.1...0.1.2) 99 | 100 | ## [0.1.1](https://github.com/nhannht/obsidian-smart-gantt/tree/0.1.1) (2024-05-16) 101 | 102 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.8...0.1.1) 103 | 104 | ## [0.0.8](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.8) (2024-04-12) 105 | 106 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.7...0.0.8) 107 | 108 | ## [0.0.7](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.7) (2024-04-11) 109 | 110 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.6...0.0.7) 111 | 112 | ## [0.0.6](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.6) (2024-04-11) 113 | 114 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.5...0.0.6) 115 | 116 | ## [0.0.5](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.5) (2024-04-11) 117 | 118 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.4...0.0.5) 119 | 120 | ## [0.0.4](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.4) (2024-04-11) 121 | 122 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.2...0.0.4) 123 | 124 | ## [0.0.2](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.2) (2024-04-11) 125 | 126 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/0.0.3...0.0.2) 127 | 128 | ## [0.0.3](https://github.com/nhannht/obsidian-smart-gantt/tree/0.0.3) (2024-04-11) 129 | 130 | [Full Changelog](https://github.com/nhannht/obsidian-smart-gantt/compare/48fff90644101a5ecaacc3d068330652fc403245...0.0.3) 131 | 132 | 133 | 134 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 135 | -------------------------------------------------------------------------------- /src/FilterModal.ts: -------------------------------------------------------------------------------- 1 | import {App, Modal} from "obsidian"; 2 | import SmartGanttPlugin from "../main"; 3 | 4 | 5 | 6 | export class FilterModal extends Modal { 7 | override onOpen() { 8 | const {contentEl} = this 9 | let allParentPath: Set = new Set() 10 | this.thisPlugin.app.vault.getMarkdownFiles().forEach(file => { 11 | file.parent?.path ? allParentPath.add(file.parent.path) : null 12 | }) 13 | let fieldSetAllFilesOrCurrentFile = contentEl.createEl("div", { 14 | cls: "smart-gantt-filter-modal-fieldset" 15 | } 16 | ) 17 | 18 | const directoryContainer = contentEl.createEl("div", { 19 | cls: "smart-gantt-filter-modal-directory-container" 20 | }) 21 | 22 | 23 | let allFileCheckBoxContainer = fieldSetAllFilesOrCurrentFile.createEl("div", { 24 | cls: "smart-gantt-checkbox-element-container" 25 | }) 26 | 27 | let currentFileCheckBoxContainer = fieldSetAllFilesOrCurrentFile.createEl("div", { 28 | cls: "smart-gantt-checkbox-element-container" 29 | }) 30 | 31 | let customPathCheckBoxContainer = fieldSetAllFilesOrCurrentFile.createEl("div", { 32 | cls: "smart-gantt-checkbox-element-container" 33 | }) 34 | 35 | let allFileCheckBox = allFileCheckBoxContainer.createEl("input", { 36 | type: "radio", 37 | attr: { 38 | id: `smart-gantt-path-filter-AllFile`, 39 | name: "all-or-current" 40 | } 41 | }) 42 | allFileCheckBoxContainer.createEl("label", { 43 | text: "All Files in Vault", 44 | attr: { 45 | for: `smart-gantt-path-filter-AllFile` 46 | } 47 | }) 48 | 49 | allFileCheckBox.addEventListener("change", async () => { 50 | const customPathCheckboxs = document.getElementsByClassName("smart-gantt-custom-path-checkbox") 51 | if (allFileCheckBox.checked) { 52 | await this.thisPlugin.settingManager.setToAllFiles() 53 | Array.from(customPathCheckboxs).forEach(element => { 54 | //@ts-ignore 55 | element.disabled = true 56 | 57 | }) 58 | directoryContainer.hidden = true 59 | directoryContainer.addClass("opacity-50") 60 | } 61 | await this.thisPlugin.helper.reloadView() 62 | }) 63 | 64 | if (this.thisPlugin.settingManager.settings.pathListFilter.indexOf("AllFiles") !== -1) { 65 | allFileCheckBox.checked = true 66 | directoryContainer.hidden = true 67 | directoryContainer.addClass("opacity-50") 68 | } 69 | 70 | 71 | let currentFileCheckBox = currentFileCheckBoxContainer.createEl("input", { 72 | type: "radio", 73 | attr: { 74 | id: `smart-gantt-path-filter-CurrentFile`, 75 | name: "all-or-current" 76 | } 77 | }) 78 | currentFileCheckBoxContainer.createEl("label", { 79 | text: "Current File", 80 | attr: { 81 | for: `smart-gantt-path-filter-CurrentFile` 82 | } 83 | }) 84 | currentFileCheckBox.addEventListener("change", async () => { 85 | const customPathCheckboxs = document.getElementsByClassName("smart-gantt-custom-path-checkbox") 86 | if (currentFileCheckBox.checked) { 87 | await this.thisPlugin.settingManager.setToCurrentFiles() 88 | Array.from(customPathCheckboxs).forEach(element => { 89 | // @ts-ignore 90 | element.disabled = true 91 | }) 92 | directoryContainer.hidden = true 93 | directoryContainer.addClass("opacity-50") 94 | } 95 | await this.thisPlugin.helper.reloadView() 96 | }) 97 | 98 | if (this.thisPlugin.settingManager.settings.pathListFilter.indexOf("CurrentFile") !== -1) { 99 | currentFileCheckBox.checked = true 100 | directoryContainer.hidden = true 101 | directoryContainer.addClass("opacity-50") 102 | } 103 | 104 | let customSelectedPathsCheckBox = customPathCheckBoxContainer.createEl("input", { 105 | type: "radio", 106 | attr: { 107 | id: "smart-gantt-filter-custom-paths", 108 | name: "all-or-current" 109 | } 110 | 111 | }) 112 | customPathCheckBoxContainer.createEl("label", { 113 | text: "Custom parent directories", 114 | attr: { 115 | for: "smart-gantt-filter-custom-paths" 116 | } 117 | }) 118 | 119 | customSelectedPathsCheckBox.addEventListener("change", async () => { 120 | const customPathCheckboxs = document.getElementsByClassName("smart-gantt-custom-path-checkbox") 121 | if (customSelectedPathsCheckBox.checked) { 122 | await this.thisPlugin.settingManager.clearAllPath() 123 | Array.from(customPathCheckboxs).forEach(e => { 124 | //@ts-ignore 125 | e.removeAttribute("disabled") 126 | }) 127 | directoryContainer.hidden = false 128 | directoryContainer.removeClass("opacity-50") 129 | } 130 | await this.thisPlugin.helper.reloadView() 131 | }) 132 | 133 | 134 | if ( 135 | this.thisPlugin.settingManager.settings.pathListFilter.indexOf("CurrentFile") === -1 && 136 | this.thisPlugin.settingManager.settings.pathListFilter.indexOf("AllFiles") === -1) { 137 | customSelectedPathsCheckBox.checked = true 138 | directoryContainer.hidden = false 139 | directoryContainer.removeClass("opacity-50") 140 | } 141 | 142 | Array.from(allParentPath).forEach((path, pathIndex) => { 143 | let singlePathContainer = directoryContainer.createEl("div", { 144 | cls: "smart-gantt-checkbox-element-container" 145 | }) 146 | let singlePathCheckbox = singlePathContainer.createEl("input", { 147 | type: "checkbox", 148 | cls: "smart-gantt-custom-path-checkbox", 149 | attr: { 150 | id: `smart-gantt-path-filter-${pathIndex}` 151 | } 152 | }) 153 | 154 | singlePathContainer.createEl("label", { 155 | text: path, 156 | attr: { 157 | for: `smart-gantt-path-filter-${pathIndex}` 158 | } 159 | }) 160 | 161 | singlePathCheckbox.addEventListener("change", async () => { 162 | if (singlePathCheckbox.checked) { 163 | await this.thisPlugin.settingManager.addPath(path) 164 | } else { 165 | await this.thisPlugin.settingManager.removePath(path) 166 | } 167 | await this.thisPlugin.helper.reloadView() 168 | // console.log(this.thisPlugin.settingManager.settings) 169 | }) 170 | 171 | if (this.thisPlugin.settingManager.settings.pathListFilter.indexOf("CurrentFile") !== -1 || 172 | this.thisPlugin.settingManager.settings.pathListFilter.indexOf("AllFiles") !== -1) { 173 | singlePathCheckbox.disabled = true 174 | } else if (customSelectedPathsCheckBox.checked) { 175 | singlePathCheckbox.disabled = false 176 | } 177 | if (this.thisPlugin.settingManager.settings.pathListFilter.indexOf(path) !== -1) { 178 | singlePathCheckbox.setAttr("checked", "checked") 179 | } 180 | }) 181 | } 182 | 183 | 184 | constructor(app: App, 185 | private thisPlugin: SmartGanttPlugin, 186 | 187 | ) { 188 | super(app) 189 | } 190 | 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/MarkdownProcesser.ts: -------------------------------------------------------------------------------- 1 | import {TFile} from "obsidian"; 2 | import {marked, Token} from "marked"; 3 | import SmartGanttPlugin from "../main"; 4 | import {SmartGanttSettings} from "./SettingManager"; 5 | import {Processor, unified} from "unified"; 6 | import remarkGfm from "remark-gfm"; 7 | import remarkParse from "remark-parse"; 8 | import {Node} from "unist" 9 | 10 | export interface TokenWithFile { 11 | token: Token, 12 | file: TFile, 13 | } 14 | 15 | export type NodeFromParseTree = { 16 | node: Node, 17 | file: TFile 18 | } 19 | 20 | export default class MarkdownProcesser { 21 | get nodes(): NodeFromParseTree[] { 22 | return this._nodes; 23 | } 24 | 25 | private _remarkProcessor: Processor; 26 | 27 | get currentPlugin(): SmartGanttPlugin { 28 | return this._currentPlugin; 29 | } 30 | 31 | get documents(): TokenWithFile[] { 32 | return this._documents; 33 | } 34 | 35 | private _files: TFile[]; 36 | private _documents: TokenWithFile[] = []; 37 | private _currentPlugin: SmartGanttPlugin; 38 | 39 | private _nodes: NodeFromParseTree[] = []; 40 | 41 | constructor(files: TFile[], 42 | currentPlugin: SmartGanttPlugin 43 | ) { 44 | this._files = files; 45 | this._currentPlugin = currentPlugin; 46 | //@ts-ignore 47 | this._remarkProcessor = unified().use(remarkGfm).use(remarkParse) 48 | } 49 | 50 | async parseAllFiles(settings: SmartGanttSettings) { 51 | const pathFilterSettings = settings.pathListFilter 52 | this._files.map(async (file) => { 53 | // console.log(file) 54 | if (pathFilterSettings.indexOf("AllFiles") !== -1) { 55 | } else if (pathFilterSettings.indexOf("CurrentFile") !== -1) { 56 | if (this._currentPlugin.app.workspace.getActiveFile()?.name !== file.name) return 57 | } else if ( 58 | (pathFilterSettings.indexOf("AllFiles") === -1) && 59 | (pathFilterSettings.indexOf("CurrentFile") === -1) && 60 | (pathFilterSettings.indexOf(file.parent?.path!) === -1) 61 | ) return 62 | // console.log(file) 63 | await this.parseFilesAndUpdateTokens(file, settings) 64 | }) 65 | } 66 | 67 | // private filterHTMLAndEmphasis(text: string) { 68 | // const stripHTML = text.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, ""), 69 | // stripEm1 = stripHTML.replace(/\*{1,3}(.*?)\*{1,3}/g, "$1"), 70 | // stripEm2 = stripEm1.replace(/_{1,3}(.*?)_{1,3}/g, "$1"), 71 | // stripStrike = stripEm2.replace(/~{1,2}(.*?)~{1,2}/g, "$1"), 72 | // stripLink = stripStrike.replace(/!?\[(.*?)]\((.*?)\)/g, "").replace(/!?\[\[(.*?)]]/g, ""); 73 | // 74 | // return stripLink 75 | // 76 | // } 77 | private async recursiveGetListItemFromParseTree(node: Node 78 | , file: TFile 79 | , settings: SmartGanttSettings) { 80 | 81 | if (node.type == "listItem") { 82 | //@ts-ignore 83 | if (settings.doneShowQ && node.checked === true || settings.todoShowQ && node.checked === false) { 84 | this.nodes.push({ 85 | node, 86 | file 87 | }) 88 | } 89 | } 90 | if ("children" in node) { 91 | //@ts-ignore 92 | node.children.forEach((childNode: Node) => { 93 | this.recursiveGetListItemFromParseTree(childNode, file, settings) 94 | }) 95 | } 96 | } 97 | 98 | private async parseFilesAndUpdateTokensNg(file: TFile, settings: SmartGanttSettings) { 99 | if (!file) return 100 | const fileContent = await this.currentPlugin.app.vault.cachedRead(file) 101 | const parseTree: Node = this._remarkProcessor.parse(fileContent) 102 | // console.log(parseTree) 103 | await this.recursiveGetListItemFromParseTree(parseTree, file, settings) 104 | 105 | } 106 | 107 | 108 | async parseAllFilesNg(settings: SmartGanttSettings) { 109 | const pathFilterSettings = settings.pathListFilter 110 | this._files.map(async (file) => { 111 | // console.log(file) 112 | if (pathFilterSettings.indexOf("AllFiles") !== -1) { 113 | } else if (pathFilterSettings.indexOf("CurrentFile") !== -1) { 114 | if (this._currentPlugin.app.workspace.getActiveFile()?.name !== file.name) return 115 | } else if ( 116 | (pathFilterSettings.indexOf("AllFiles") === -1) && 117 | (pathFilterSettings.indexOf("CurrentFile") === -1) && 118 | (pathFilterSettings.indexOf(file.parent?.path!) === -1) 119 | ) return 120 | // console.log(file) 121 | await this.parseFilesAndUpdateTokensNg(file, settings) 122 | }) 123 | } 124 | 125 | 126 | private async parseFilesAndUpdateTokens(file: TFile, settings: SmartGanttSettings) { 127 | if (!file) { 128 | return 129 | } 130 | const fileContent = await this.currentPlugin.app.vault.cachedRead(file) 131 | 132 | 133 | // const fileContentStripHTML = this.filterHTMLAndEmphasis(fileContent) 134 | 135 | 136 | // console.log(fileContentStripHTML) 137 | 138 | const lexerResult = marked.lexer(fileContent); 139 | 140 | // console.log(lexerResult) 141 | 142 | 143 | lexerResult.map((token) => { 144 | 145 | this.recusiveGetToken(token, this._documents, file, settings) 146 | }) 147 | // filter token which is the smallest modulo 148 | 149 | 150 | } 151 | 152 | private recusiveGetToken(document: Token, 153 | tokens: TokenWithFile[], 154 | file: TFile, 155 | settings: SmartGanttSettings 156 | ) { 157 | //@ts-ignore 158 | if ("type" in document && document.task === true && document.type === "list_item") { 159 | if (document.raw.search("\n") !== -1) { 160 | document.text = document.text.split("\n")[0] 161 | document.raw = document.raw.split("\n")[0] 162 | } 163 | // console.log(document) 164 | 165 | if (settings.doneShowQ && (document.checked === true)) { 166 | tokens.push({ 167 | token: document, 168 | file: file 169 | 170 | }) 171 | } 172 | 173 | if (settings.todoShowQ && (document.checked === false)) { 174 | tokens.push({ 175 | token: document, 176 | file: file, 177 | 178 | }) 179 | } 180 | 181 | 182 | } 183 | if ("tokens" in document && document.tokens) { 184 | 185 | document.tokens.map((t) => { 186 | this.recusiveGetToken(t, tokens, file, settings) 187 | }) 188 | // table 189 | } 190 | if ("rows" in document && document.rows) { 191 | document.rows.map((row: any[]) => { 192 | row.map((cell) => { 193 | this.recusiveGetToken(cell, tokens, file, settings) 194 | }) 195 | }) 196 | } 197 | if ("header" in document && document.header) { 198 | 199 | 200 | document.header.map((header: any[]) => { 201 | // @ts-ignore 202 | this.recusiveGetToken(header, tokens) 203 | }) 204 | } 205 | // for list 206 | if ("items" in document && document.items) { 207 | document.items.map((item: any) => { 208 | this.recusiveGetToken(item, tokens, file, settings) 209 | }) 210 | } 211 | 212 | // filter only document which is the most module 213 | 214 | } 215 | 216 | 217 | } 218 | -------------------------------------------------------------------------------- /src/component/SettingViewComponent.tsx: -------------------------------------------------------------------------------- 1 | import {SmartGanttSettings} from "../SettingManager"; 2 | import {useState} from "react"; 3 | // import {usePlugin} from "./ReactContext"; 4 | import {Checkbox} from "./Checkbox"; 5 | import {Label} from "./Label"; 6 | import {RadioGroup, RadioGroupItem} from "./RadioGroup"; 7 | import SmartGanttPlugin from "../../main"; 8 | import {ScrollArea} from "./ScrollableList"; 9 | import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "./Select"; 10 | import {ViewMode} from "gantt-task-react"; 11 | import {Button} from "./Button"; 12 | 13 | 14 | 15 | const SettingViewComponent = (props: { 16 | inputS: SmartGanttSettings|undefined, 17 | saveSettings: (s: SmartGanttSettings) => void, 18 | isSettingsQ: boolean, 19 | isSettingsQHandle: (b: boolean) => void, 20 | updateSettingInCodeBlockHandle?: (s: SmartGanttSettings) => void, 21 | thisPlugin: SmartGanttPlugin 22 | 23 | }) => { 24 | 25 | const [s, setS] = useState(structuredClone(props.inputS)) 26 | // const thisPlugin = usePlugin() 27 | 28 | const allFileFilterRadio =
29 | 35 |
; 36 | 37 | const currentFileFilterRadio =
38 | 44 | 45 | 46 |
; 47 | const customPathFilterRadio =
48 | 55 | 56 | 57 | 58 |
; 59 | const customPathListCheckboxs = ; 98 | 99 | 100 | const filterBaseOnStatusCheckbox =
103 |
104 | { 106 | setS({...s, todoShowQ: Boolean(e)}) 107 | 108 | }} 109 | id={"TodoQ"} 110 | checked={s.todoShowQ} 111 | 112 | > 113 | 115 |
116 |
117 | { 119 | setS({...s, doneShowQ: Boolean(e)}) 120 | }} 121 | id={"DoneQ"} 122 | checked={s.doneShowQ} 123 | 124 | > 125 | 126 |
127 |
; 128 | 129 | let settingButton = 142 | } else { 143 | settingButton = 150 | 151 | 152 | } 153 | const cancelButton = 160 | const buttonsPanel =
161 | {settingButton} 162 | {props.isSettingsQ ? cancelButton : null} 163 |
164 | 165 | const viewModeSelect = () => { 166 | return 191 | } 192 | 193 | const showTaskListInChartCheckbox = ()=>{ 194 | return
195 | { 197 | setS({...s,leftBarChartDisplayQ: Boolean(e)}) 198 | }} 199 | checked={s.leftBarChartDisplayQ} 200 | id={"showtasklistinchartcheckbox"} 201 | /> 202 | 203 |
204 | } 205 | 206 | 207 | const settingView = <> 208 | {buttonsPanel} 209 |
{ 210 | if (e === "AllFiles" || e === "CurrentFile") { 211 | setS({...s, pathListFilter: [e]}) 212 | 213 | } else if (e === "CustomPath") { 214 | setS({...s, pathListFilter: []}) 215 | } 216 | }}> 217 | {allFileFilterRadio} 218 | {currentFileFilterRadio} 219 | {customPathFilterRadio} 220 | 221 | {filterBaseOnStatusCheckbox} 222 | {viewModeSelect()} 223 | {showTaskListInChartCheckbox()} 224 | 225 |
226 | 227 | 228 | {customPathListCheckboxs} 229 | 230 | 231 | return <> 232 | {settingView} 233 | 234 | 235 | } 236 | 237 | export default SettingViewComponent 238 | -------------------------------------------------------------------------------- /src/addon/mode/simple.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function (mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function (CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineSimpleMode = function (name, states) { 15 | CodeMirror.defineMode(name, function (config) { 16 | return CodeMirror.simpleMode(config, states); 17 | }); 18 | }; 19 | 20 | CodeMirror.simpleMode = function (config, states) { 21 | ensureState(states, "start"); 22 | var states_ = {}, meta = states.meta || {}, hasIndentation = false; 23 | for (var state in states) if (state != meta && states.hasOwnProperty(state)) { 24 | var list = states_[state] = [], orig = states[state]; 25 | for (var i = 0; i < orig.length; i++) { 26 | var data = orig[i]; 27 | list.push(new Rule(data, states)); 28 | if (data.indent || data.dedent) hasIndentation = true; 29 | } 30 | } 31 | var mode = { 32 | startState: function () { 33 | return { 34 | state: "start", pending: null, 35 | local: null, localState: null, 36 | indent: hasIndentation ? [] : null 37 | }; 38 | }, 39 | copyState: function (state) { 40 | var s = { 41 | state: state.state, pending: state.pending, 42 | local: state.local, localState: null, 43 | indent: state.indent && state.indent.slice(0) 44 | }; 45 | if (state.localState) 46 | s.localState = CodeMirror.copyState(state.local.mode, state.localState); 47 | if (state.stack) 48 | s.stack = state.stack.slice(0); 49 | for (var pers = state.persistentStates; pers; pers = pers.next) 50 | s.persistentStates = { 51 | mode: pers.mode, 52 | spec: pers.spec, 53 | state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state), 54 | next: s.persistentStates 55 | }; 56 | return s; 57 | }, 58 | token: tokenFunction(states_, config), 59 | innerMode: function (state) { 60 | return state.local && {mode: state.local.mode, state: state.localState}; 61 | }, 62 | indent: indentFunction(states_, meta) 63 | }; 64 | if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop)) 65 | mode[prop] = meta[prop]; 66 | return mode; 67 | }; 68 | 69 | function ensureState(states, name) { 70 | if (!states.hasOwnProperty(name)) 71 | throw new Error("Undefined state " + name + " in simple mode"); 72 | } 73 | 74 | function toRegex(val, caret) { 75 | if (!val) return /(?:)/; 76 | var flags = ""; 77 | if (val instanceof RegExp) { 78 | if (val.ignoreCase) flags = "i"; 79 | val = val.source; 80 | } else { 81 | val = String(val); 82 | } 83 | return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags); 84 | } 85 | 86 | function asToken(val) { 87 | if (!val) return null; 88 | if (val.apply) return val 89 | if (typeof val == "string") return val.replace(/\./g, " "); 90 | var result = []; 91 | for (var i = 0; i < val.length; i++) 92 | result.push(val[i] && val[i].replace(/\./g, " ")); 93 | return result; 94 | } 95 | 96 | function Rule(data, states) { 97 | if (data.next || data.push) ensureState(states, data.next || data.push); 98 | this.regex = toRegex(data.regex); 99 | this.token = asToken(data.token); 100 | this.data = data; 101 | } 102 | 103 | function tokenFunction(states, config) { 104 | return function (stream, state) { 105 | if (state.pending) { 106 | var pend = state.pending.shift(); 107 | if (state.pending.length == 0) state.pending = null; 108 | stream.pos += pend.text.length; 109 | return pend.token; 110 | } 111 | 112 | if (state.local) { 113 | if (state.local.end && stream.match(state.local.end)) { 114 | var tok = state.local.endToken || null; 115 | state.local = state.localState = null; 116 | return tok; 117 | } else { 118 | var tok = state.local.mode.token(stream, state.localState), m; 119 | if (state.local.endScan && (m = state.local.endScan.exec(stream.current()))) 120 | stream.pos = stream.start + m.index; 121 | return tok; 122 | } 123 | } 124 | 125 | var curState = states[state.state]; 126 | for (var i = 0; i < curState.length; i++) { 127 | var rule = curState[i]; 128 | var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); 129 | if (matches) { 130 | if (rule.data.next) { 131 | state.state = rule.data.next; 132 | } else if (rule.data.push) { 133 | (state.stack || (state.stack = [])).push(state.state); 134 | state.state = rule.data.push; 135 | } else if (rule.data.pop && state.stack && state.stack.length) { 136 | state.state = state.stack.pop(); 137 | } 138 | 139 | if (rule.data.mode) 140 | enterLocalMode(config, state, rule.data.mode, rule.token); 141 | if (rule.data.indent) 142 | state.indent.push(stream.indentation() + config.indentUnit); 143 | if (rule.data.dedent) 144 | state.indent.pop(); 145 | var token = rule.token 146 | if (token && token.apply) token = token(matches) 147 | if (matches.length > 2 && rule.token && typeof rule.token != "string") { 148 | state.pending = []; 149 | for (var j = 2; j < matches.length; j++) 150 | if (matches[j]) 151 | state.pending.push({text: matches[j], token: rule.token[j - 1]}); 152 | stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); 153 | return token[0]; 154 | } else if (token && token.join) { 155 | return token[0]; 156 | } else { 157 | return token; 158 | } 159 | } 160 | } 161 | stream.next(); 162 | return null; 163 | }; 164 | } 165 | 166 | function cmp(a, b) { 167 | if (a === b) return true; 168 | if (!a || typeof a != "object" || !b || typeof b != "object") return false; 169 | var props = 0; 170 | for (var prop in a) if (a.hasOwnProperty(prop)) { 171 | if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false; 172 | props++; 173 | } 174 | for (var prop in b) if (b.hasOwnProperty(prop)) props--; 175 | return props == 0; 176 | } 177 | 178 | function enterLocalMode(config, state, spec, token) { 179 | var pers; 180 | if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next) 181 | if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p; 182 | var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec); 183 | var lState = pers ? pers.state : CodeMirror.startState(mode); 184 | if (spec.persistent && !pers) 185 | state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates}; 186 | 187 | state.localState = lState; 188 | state.local = { 189 | mode: mode, 190 | end: spec.end && toRegex(spec.end), 191 | endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false), 192 | endToken: token && token.join ? token[token.length - 1] : token 193 | }; 194 | } 195 | 196 | function indexOf(val, arr) { 197 | for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true; 198 | } 199 | 200 | function indentFunction(states, meta) { 201 | return function (state, textAfter, line) { 202 | if (state.local && state.local.mode.indent) 203 | return state.local.mode.indent(state.localState, textAfter, line); 204 | if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1) 205 | return CodeMirror.Pass; 206 | 207 | var pos = state.indent.length - 1, rules = states[state.state]; 208 | scan: for (; ;) { 209 | for (var i = 0; i < rules.length; i++) { 210 | var rule = rules[i]; 211 | if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { 212 | var m = rule.regex.exec(textAfter); 213 | if (m && m[0]) { 214 | pos--; 215 | if (rule.next || rule.push) rules = states[rule.next || rule.push]; 216 | textAfter = textAfter.slice(m[0].length); 217 | continue scan; 218 | } 219 | } 220 | } 221 | break; 222 | } 223 | return pos < 0 ? 0 : state.indent[pos]; 224 | }; 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /src/BlockComponent/SmartGanttBlockReactComponent.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | import {loadMermaid, MarkdownPostProcessorContext} from "obsidian"; 3 | import SmartGanttPlugin from "../../main"; 4 | import {SmartGanttSettings} from "../SettingManager"; 5 | import MarkdownProcesser from "../MarkdownProcesser"; 6 | import TimelineExtractor, {TimelineExtractorResult} from "../TimelineExtractor"; 7 | import {Chrono} from "chrono-node"; 8 | import MermaidCrafter from "../lib/MermaidCrafter"; 9 | import {useMeasure} from "react-use"; 10 | 11 | 12 | export const SmartGanttBlockReactComponent = (props: { 13 | ctx: MarkdownPostProcessorContext, 14 | src: string, 15 | thisPlugin: SmartGanttPlugin, 16 | settings: SmartGanttSettings 17 | }) => { 18 | const [internalSettings, setInternalSettings] = 19 | useState(structuredClone(props.settings)) 20 | const [isSettingQ, setIsSettingQ] = useState(false) 21 | const [craft, setCraft] = useState("") 22 | const [resultWithChronoCount, setResultWithChronoCount] = useState(0) 23 | const [timelineResults, setTimelineResults] = useState([]) 24 | const [appStyle,] = useState(getComputedStyle(document.body)) 25 | const [blockContainerComponentRef, blockContainerComponentMeasure] = useMeasure() 26 | 27 | const countResultWithChrono = (results: TimelineExtractorResult[]) => { 28 | setResultWithChronoCount(0) 29 | results.forEach(r => { 30 | if (r.parsedResultsAndRawText.parsedResults) { 31 | setResultWithChronoCount(resultWithChronoCount + 1) 32 | 33 | } 34 | }) 35 | 36 | } 37 | 38 | const updateBlockSettingWithInternalSetting = (settingObject: SmartGanttSettings, 39 | context: MarkdownPostProcessorContext) => { 40 | 41 | const sourcePath = context.sourcePath 42 | //@ts-ignore 43 | const elInfo = context.getSectionInfo(context.el) 44 | // console.log(elInfo) 45 | if (elInfo) { 46 | // console.log(elInfo.text) 47 | let linesFromFile = elInfo.text.split(/(.*?\n)/g) 48 | linesFromFile.forEach((e, i) => { 49 | if (e === "") linesFromFile.splice(i, 1) 50 | }) 51 | // console.log(linesFromFile) 52 | linesFromFile.splice(elInfo.lineStart + 1, 53 | elInfo.lineEnd - elInfo.lineStart - 1, 54 | JSON.stringify(settingObject, null, "\t"), "\n") 55 | // console.log(linesFromFile) 56 | const newSettingsString = linesFromFile.join("") 57 | const file = props.thisPlugin.app.vault.getFileByPath(sourcePath) 58 | if (file) { 59 | props.thisPlugin.app.vault.modify(file, newSettingsString) 60 | } 61 | } 62 | 63 | } 64 | 65 | 66 | useEffect(() => { 67 | (async () => { 68 | const allMarkdownFiles = props.thisPlugin.app.vault.getMarkdownFiles(); 69 | const markdownProcesser = new MarkdownProcesser(allMarkdownFiles, props.thisPlugin) 70 | await markdownProcesser.parseAllFiles(internalSettings) 71 | const allSentences = markdownProcesser.documents 72 | // console.log(allSentences) 73 | const timelineExtractor = new TimelineExtractor(new Chrono()) 74 | const parsedResults = await timelineExtractor.GetTimelineDataFromDocumentArrayWithChrono(allSentences) 75 | // console.log(parsedResult) 76 | countResultWithChrono(parsedResults) 77 | // console.log(resultWithChronoCount) 78 | setTimelineResults(parsedResults) 79 | // const timelineData = timelineExtractor.timelineData 80 | const mermaidCrafter = new MermaidCrafter(props.thisPlugin) 81 | setCraft(mermaidCrafter.craftMermaid(parsedResults)) 82 | 83 | // updateBlockSettingWithInternalSetting(internalSettings, props.ctx) 84 | 85 | })() 86 | }, [internalSettings]); 87 | 88 | 89 | const getAllParentPath = () => { 90 | if (props.thisPlugin) { 91 | let allParentPath: Set = new Set() 92 | props.thisPlugin?.app.vault.getMarkdownFiles().forEach(r => { 93 | r.parent?.path ? allParentPath.add(r.parent.path) : null 94 | }) 95 | return Array.from(allParentPath) 96 | } 97 | return [] 98 | } 99 | 100 | let mainComponent = <> 101 | 102 | const allFileFilterRadio =
{ 104 | if (e.target.checked) { 105 | let tempSetting: SmartGanttSettings = structuredClone(internalSettings) 106 | tempSetting.pathListFilter = ["AllFiles"] 107 | setInternalSettings(tempSetting) 108 | } 109 | }} 110 | name={"pathFilterRadio"} 111 | type={"radio"} 112 | id={"allFiles"} 113 | checked={internalSettings.pathListFilter.indexOf("AllFiles") !== -1} 114 | > 115 |
; 116 | const currentFileFilterRadio =
117 | { 119 | if (e.target.checked) { 120 | let tempSetting: SmartGanttSettings = structuredClone(internalSettings) 121 | tempSetting.pathListFilter = ["CurrentFile"] 122 | setInternalSettings(tempSetting) 123 | } 124 | }} 125 | name={"pathFilterRadio"} type={"radio"} id={"currentFile"} 126 | checked={internalSettings.pathListFilter.indexOf("CurrentFile") !== -1} 127 | > 128 | 129 | 130 |
; 131 | 132 | const customPathFilterRadio =
{ 134 | if (e.target.checked) { 135 | const temp: SmartGanttSettings = structuredClone(internalSettings) 136 | temp.pathListFilter = [] 137 | setInternalSettings(temp) 138 | } 139 | 140 | }} 141 | name={"pathFilterRadio"} type={"radio"} id={"customPath"} 142 | checked={internalSettings.pathListFilter.indexOf("AllFiles") === -1 && 143 | internalSettings.pathListFilter.indexOf("CurrentFile") === -1 144 | }> 145 | 146 | 147 | 148 |
; 149 | const customPathListCheckboxs = ; 178 | const filterBaseOnStatusCheckbox =
181 |
182 | { 184 | let temp: SmartGanttSettings = structuredClone(internalSettings) 185 | temp.todoShowQ = e.target.checked; 186 | setInternalSettings(temp) 187 | 188 | }} 189 | type={"checkbox"} id={"TodoQ"} 190 | checked={internalSettings.todoShowQ} 191 | 192 | > 193 | 194 |
195 |
196 | { 198 | let temp: SmartGanttSettings = structuredClone(internalSettings) 199 | temp.doneShowQ = e.target.checked; 200 | setInternalSettings(temp) 201 | }} 202 | type={"checkbox"} id={"DoneQ"} 203 | checked={internalSettings.doneShowQ} 204 | 205 | > 206 | 207 |
208 |
; 209 | 210 | 211 | let mermaidComponent = <> 212 | if (resultWithChronoCount === 0) { 213 | // console.log(timelineResults) 214 | let taskStrings: string[] = [] 215 | timelineResults.forEach(r => { 216 | taskStrings.push(r.token.raw) 217 | }) 218 | 219 | mermaidComponent = <>We don't have any tasks that can extract time to plot 220 |
221 | { 222 | taskStrings.length !== 0 ? 223 | <> Current we have these task 224 |
225 | { 226 | taskStrings.join("\n") 227 | } 228 | : 229 | <>There is no line with checkbox in target files 230 | } 231 | 232 | } else { 233 | mermaidComponent =
234 | 				{craft}
235 | 			
236 | 237 | } 238 | 239 | 240 | let settingButton = 250 | } else { 251 | settingButton = 258 | } 259 | 260 | const cancelButton = 266 | 267 | const buttonsPanel =
268 | {settingButton} 269 | {isSettingQ ? cancelButton : null} 270 |
271 | 272 | const settingView = <> 273 | {buttonsPanel} 274 |
275 | {allFileFilterRadio} 276 | {currentFileFilterRadio} 277 | {customPathFilterRadio} 278 | {filterBaseOnStatusCheckbox} 279 |
280 | 281 | {customPathListCheckboxs} 282 | 283 | 284 | 285 | if (isSettingQ) { 286 | mainComponent =
287 | {settingView} 288 |
289 | } else { 290 | loadMermaid() 291 | .then(mermaid => { 292 | mermaid.initialize({ 293 | startOnLoad: true, 294 | maxTextSize: 99999999, 295 | theme:'forest', 296 | themeCSS:` 297 | .grid .tick { 298 | stroke: lightgrey; 299 | opacity: 0.3; 300 | shape-rendering: crispEdges; 301 | } 302 | 303 | 304 | .taskText.clickable { 305 | fill: ${appStyle.getPropertyValue("--text-normal")} !important; 306 | text-anchor: middle; 307 | } 308 | 309 | .taskTextOutsideRight.clickable { 310 | fill: ${appStyle.getPropertyValue("--text-normal")} !important; 311 | 312 | } 313 | .taskTextOutsideLeft.clickable { 314 | fill: ${appStyle.getPropertyValue("--text-normal")} !important ; 315 | text-anchor: end; 316 | } 317 | 318 | 319 | .sectionTitle { 320 | fill: ${appStyle.getPropertyValue("--text-muted")} !important; 321 | } 322 | 323 | text { 324 | fill: ${appStyle.getPropertyValue("--text-normal")} !important; 325 | } 326 | ` 327 | }); 328 | mermaid.contentLoaded(); 329 | }) 330 | 331 | mainComponent =
{ 333 | setIsSettingQ(true) 334 | }} 335 | > 336 | {mermaidComponent} 337 |
338 | } 339 | 340 | //@ts-ignore 341 | return
342 | {blockContainerComponentMeasure.width > 0 ? mainComponent : <>} 343 |
344 | }; 345 | -------------------------------------------------------------------------------- /src/GanttItemView.tsx: -------------------------------------------------------------------------------- 1 | import {FileView, Modal, Notice, Setting, TFile, ViewStateResult, WorkspaceLeaf} from "obsidian"; 2 | import SmartGanttPlugin from "../main"; 3 | import HelperNg from "@/HelperNg"; 4 | import {Node} from "unist" 5 | import {v4} from "uuid"; 6 | import moment from "moment"; 7 | import {Gantt, Task} from "gantt-task-react"; 8 | import {createRoot, Root} from "react-dom/client"; 9 | import {createContext, StrictMode, useContext, useState} from "react"; 10 | 11 | export class TaskCustomizeModal extends Modal { 12 | constructor(view: SmartGanttItemView, task: SmartGanttTask, onSubmit: (task: SmartGanttTask) => void) { 13 | super(view.plugin.app); 14 | this.setTitle("Customize tasks") 15 | let cloneTask = {...task} 16 | 17 | let nameSetting = new Setting(this.contentEl) 18 | .setName('Task name') 19 | .addText((text) => { 20 | text 21 | 22 | .setPlaceholder("Choose new name") 23 | .setValue(task.name) 24 | .onChange((value) => { 25 | cloneTask.name = value 26 | }) 27 | }) 28 | 29 | let startDateSetting = new Setting(this.contentEl) 30 | .setName("Start date") 31 | .addMomentFormat((com) => { 32 | com 33 | .setValue(moment(task.start).format("YYYY-MM-DD")) 34 | .onChange(value => { 35 | if (moment(value).isValid()) { 36 | cloneTask.start = moment(value).toDate() 37 | 38 | } else { 39 | new Notice("Invalid date", 5000) 40 | } 41 | }) 42 | }) 43 | 44 | let endDateSetting = new Setting(this.contentEl) 45 | .setName("End date") 46 | .addMomentFormat((com) => { 47 | com 48 | .setValue(moment(task.end).format("YYYY-MM-DD")) 49 | .onChange(value => { 50 | if (moment(value).isValid()) { 51 | cloneTask.end = moment(value).toDate() 52 | } else { 53 | new Notice("Invalid date", 5000) 54 | } 55 | 56 | }) 57 | }) 58 | 59 | let progressSetting = new Setting(this.contentEl) 60 | .setName("Progress") 61 | .addSlider((value) => { 62 | value.setValue(task.progress) 63 | .onChange(v => { 64 | cloneTask.progress = v 65 | }) 66 | .setDynamicTooltip() 67 | }) 68 | 69 | new Setting(this.contentEl) 70 | .addButton((btn) => { 71 | btn.setButtonText("Save") 72 | .setCta() 73 | .onClick(() => { 74 | this.close() 75 | onSubmit(cloneTask) 76 | }) 77 | }) 78 | .addButton((btn) => { 79 | btn.setButtonText("Cancel") 80 | .onClick(() => { 81 | this.close() 82 | }) 83 | }) 84 | .addButton((btn) => { 85 | btn.setButtonText("Backlog") 86 | .setTooltip("Hide it from this chart but the task still stay in markdown file") 87 | .onClick(() => { 88 | cloneTask.inventory = "backlog" 89 | this.close() 90 | onSubmit(cloneTask) 91 | 92 | 93 | }) 94 | }) 95 | } 96 | } 97 | 98 | export const DATE_FORMAT = "YYYY-MM-DD" 99 | 100 | export interface SmartGanttTask extends Task { 101 | id: string, 102 | name: string, 103 | start: Date, 104 | end: Date, 105 | // created: Date, 106 | // completion?: Date, 107 | dependencies: string[], 108 | progress: number, 109 | duration?: string, 110 | // important?: boolean, 111 | lineIndex: number, 112 | inventory: string, 113 | 114 | } 115 | 116 | export interface GanttOptions { 117 | infinite_padding: boolean, 118 | view_mode_select: boolean, 119 | container_height: number 120 | 121 | } 122 | 123 | export interface SmartGanttItemViewState extends Record { 124 | projectId: string, 125 | projectName: string, 126 | taskMarkDownItems: Node[] 127 | 128 | tasks: any[], 129 | file: TFile | null 130 | } 131 | 132 | 133 | const useCustomContext = () => { 134 | const context = useContext(Context) 135 | if (!context) { 136 | throw new Error("useCustomContext must be used within a Provider") 137 | } 138 | return useContext(Context) 139 | } 140 | 141 | export function MainComponent() { 142 | const {view} = useCustomContext() as { view: SmartGanttItemView } 143 | const [tasks, setTasks] = useState(view.tasks) 144 | 145 | const updateTaskInComponentAndView = (tasks: SmartGanttTask[]) => { 146 | setTasks(() => { 147 | if (view.file) { 148 | view.saveBackToFile(tasks, view.file) 149 | view.tasks = tasks 150 | } 151 | return tasks 152 | }) 153 | } 154 | 155 | const createNewTask = () => { 156 | // console.log("Yeah we create new task") 157 | let task: SmartGanttTask = { 158 | inventory: "", 159 | type: "task", 160 | id: v4(), 161 | start: moment().toDate(), 162 | end: moment().add(1, "day").toDate(), 163 | name: "New task", 164 | progress: 0, 165 | dependencies: [], 166 | lineIndex: -1 167 | } 168 | new TaskCustomizeModal(view, task, (task) => { 169 | updateTaskInComponentAndView([...tasks, task]) 170 | }).open() 171 | } 172 | 173 | const onDateChange = (task: Task) => { 174 | const newTasks: SmartGanttTask[] = tasks.map(t => { 175 | if (t.id === task.id) { 176 | return { 177 | ...t, 178 | start: task.start, 179 | end: task.end 180 | } 181 | } 182 | return t 183 | }) 184 | updateTaskInComponentAndView(newTasks) 185 | } 186 | 187 | const onProgressChange = (task: Task, children: Task[]) => { 188 | const newTasks: SmartGanttTask[] = tasks.map(t => { 189 | if (t.id === task.id) { 190 | return { 191 | ...t, 192 | progress: task.progress 193 | } 194 | } 195 | return t 196 | }) 197 | updateTaskInComponentAndView(newTasks) 198 | } 199 | 200 | return
201 |
202 | 205 |
206 | 207 | { 212 | const taskToEdit = tasks.find(t => t.id === task.id) 213 | if (taskToEdit) { 214 | new TaskCustomizeModal(view, taskToEdit, (task) => { 215 | updateTaskInComponentAndView(tasks.map(t => { 216 | if (t.id === task.id) { 217 | return task 218 | } 219 | return t 220 | })) 221 | } 222 | ).open() 223 | } 224 | 225 | } 226 | } 227 | /> 228 |
229 | } 230 | 231 | const Context = createContext(null) 234 | export const SMART_GANTT_ITEM_VIEW_TYPE = "smart-gantt-item-view"; 235 | 236 | export default class SmartGanttItemView extends FileView implements SmartGanttItemViewState { 237 | root: Root | null = null; 238 | 239 | taskMarkDownItems: Node[] = [] 240 | 241 | getViewType(): string { 242 | // console.log("Get view type called") 243 | return SMART_GANTT_ITEM_VIEW_TYPE; 244 | } 245 | 246 | constructor(leaf: WorkspaceLeaf, public plugin: SmartGanttPlugin) { 247 | // console.log("Contructor called") 248 | super(leaf); 249 | } 250 | 251 | [x: string]: unknown; 252 | 253 | 254 | override getState(): SmartGanttItemViewState { 255 | // console.log("Get state called") 256 | return { 257 | tasks: this.tasks, 258 | projectId: this.projectId, 259 | projectName: this.projectName, 260 | file: this.file?.path as unknown as TFile ?? "", // yeah, it is trick to silent ts checker 261 | taskMarkDownItems: this.taskMarkDownItems 262 | 263 | } 264 | } 265 | 266 | 267 | onunload() { 268 | // this.saveBackToFile(this.tasks) 269 | // console.log("on unload triggered") 270 | if (this.root) { 271 | this.root.unmount() 272 | } 273 | super.onunload(); 274 | } 275 | 276 | 277 | protected onClose(): Promise { 278 | // this.saveBackToFile(this.tasks) 279 | // console.log("on close triggered") 280 | if (this.root) { 281 | this.root.unmount() 282 | } 283 | return super.onClose(); 284 | } 285 | 286 | 287 | onUnloadFile(file: TFile): Promise { 288 | // this.saveBackToFile(this.tasks, file) 289 | return super.onUnloadFile(file); 290 | } 291 | 292 | async makeGanttChart() { 293 | // console.log("Make gantt chart") 294 | if (this.root) { 295 | this.root.unmount() 296 | } 297 | const container = this.containerEl.children[1] 298 | this.root = createRoot(container) 299 | this.root.render( 300 | 301 | 304 | 305 | 306 | 307 | ) 308 | 309 | } 310 | 311 | // we don't actua delete the task, just put a mark into it 312 | // markTaskAsDeleteInFile(task: SmartGanttTask, file: TFile) { 313 | // this.app.vault.read(file).then(content => { 314 | // let lines = content.split("\n") 315 | // task.inventory = "backlog" 316 | // lines[task.lineIndex] = `- [ ] ${task.name} [smartGanttId :: ${task.id}] [start::${moment(task.start).format(DATE_FORMAT)}] [due::${moment(task.end).format(DATE_FORMAT)}] [created::${moment(task.created).format(DATE_FORMAT)}] [dependencies::${task.dependencies.join(",")}] [type::${task.type}] [progress::${task.progress}] [inventory::${task.inventory}]` 317 | // 318 | // this.plugin.app.vault.modify(file, lines.join("\n")) 319 | // }) 320 | // } 321 | 322 | convertSmartGanttTaskToMarkdownString(task: SmartGanttTask) { 323 | let output = "" 324 | task.progress === 100 ? output += `- [x] ` : output += `- [ ] ` 325 | output += ` ${task.name} ` 326 | for (let key of Object.keys(task)) { 327 | 328 | if (key === "id") { 329 | output += ` [smartGanttId :: ${task["id"]}] ` 330 | 331 | } else if (key === "start") { 332 | const start = moment(task["start"]).format(DATE_FORMAT) 333 | output += ` [start :: ${start}] ` 334 | 335 | } else if (key === "end") { 336 | const due = moment(task["end"]).format(DATE_FORMAT) 337 | output += ` [due :: ${due}] ` 338 | } else if (key === "dependencies") { 339 | output += ` [dependencies :: ${task["dependencies"].join(",")}] ` 340 | } else { 341 | // @ts-ignore 342 | output += ` [${key}::${task[key]}] ` 343 | } 344 | } 345 | return output 346 | 347 | } 348 | 349 | saveBackToFile(tasks: SmartGanttTask[], file: TFile) { 350 | 351 | this.app.vault.read(file).then(content => { 352 | let lines = content.split("\n") 353 | 354 | for (const t of tasks) { 355 | if (t.lineIndex === -1) { 356 | t.lineIndex = lines.length 357 | lines.push(this.convertSmartGanttTaskToMarkdownString(t)) 358 | } else { 359 | 360 | lines[t.lineIndex] = this.convertSmartGanttTaskToMarkdownString(t) 361 | } 362 | 363 | } 364 | this.plugin.app.vault.modify(file, lines.join("\n")) 365 | 366 | }) 367 | } 368 | 369 | 370 | override async onLoadFile(file: TFile) { 371 | await super.onLoadFile(file); 372 | this.tasks = [] 373 | // console.log("On loaded file") 374 | const helper = new HelperNg(this.plugin) 375 | // console.log("We are in the file") 376 | const tasks = await helper.getAllLinesContainCheckboxInMarkdown(file) 377 | if (tasks.length === 0){ 378 | this.tasks.push({ 379 | name: "In the beginning there is only darkness", 380 | id: v4(), 381 | start: moment().toDate(), 382 | end: moment().add(3,"day").toDate(), 383 | dependencies: [], 384 | progress: 0, 385 | lineIndex: -1, 386 | type: "task", 387 | inventory: "task", 388 | }) 389 | 390 | } else { 391 | for (const task of tasks) { 392 | const taskWithMetaData = await helper.extractLineWithCheckboxToTaskWithMetaData(task) 393 | // console.log(taskWithMetaData) 394 | if (taskWithMetaData && (!taskWithMetaData.metadata.inventory || taskWithMetaData.metadata.inventory !== "backlog")) { 395 | this.tasks.push({ 396 | name: taskWithMetaData?.name ?? "No name", 397 | progress: Number(taskWithMetaData?.metadata.progress) ?? 0, 398 | id: taskWithMetaData?.metadata.smartGanttId ?? v4(), 399 | start: moment(taskWithMetaData?.metadata.start).toDate() ?? moment().toDate(), 400 | end: moment(taskWithMetaData?.metadata.due).toDate() ?? moment().add(1, "day").toDate(), 401 | // created: moment(taskWithMetaData?.metadata.created).toDate() ?? moment().toDate(), 402 | dependencies: taskWithMetaData?.metadata.dependencies.split(",") ?? [], 403 | type: taskWithMetaData?.metadata.type ?? "task", 404 | lineIndex: taskWithMetaData?.lineIndex ?? -1, 405 | inventory: taskWithMetaData?.metadata.inventory ?? "task" 406 | } as SmartGanttTask) 407 | } 408 | } 409 | 410 | } 411 | // console.log(tasks) 412 | await this.makeGanttChart() 413 | } 414 | 415 | override setState(state: SmartGanttItemViewState, result: ViewStateResult): Promise { 416 | // console.log("Set state") 417 | this.projectName = state.projectName; 418 | this.projectId = state.projectId 419 | this.tasks = state.tasks 420 | this.file = state.file 421 | this.taskMarkDownItems = state.taskMarkDownItems 422 | 423 | return super.setState(state, result); 424 | } 425 | 426 | projectId = ""; 427 | projectName = ""; 428 | 429 | tasks: SmartGanttTask[] = []; 430 | 431 | } 432 | -------------------------------------------------------------------------------- /src/mode/gantt/gantt-list.js: -------------------------------------------------------------------------------- 1 | (function(mod) { 2 | if (typeof exports == "object" && typeof module == "object") // CommonJS 3 | mod(require("../../lib/codemirror")); 4 | else if (typeof define == "function" && define.amd) // AMD 5 | define(["../../lib/codemirror"], mod); 6 | else // Plain browser env 7 | mod(CodeMirror); 8 | })(function(CodeMirror) { 9 | "use strict"; 10 | CodeMirror.defineMode("gantt-list", function(config, parserConfig) { 11 | var indentUnit = config.indentUnit; 12 | var statementIndent = parserConfig.statementIndent; 13 | var jsonldMode = parserConfig.jsonld; 14 | var jsonMode = parserConfig.json || jsonldMode; 15 | var isTS = parserConfig.typescript; 16 | var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; 17 | 18 | // Tokenizer 19 | 20 | var keywords = function(){ 21 | function kw(type) {return {type: type, style: "keyword"};} 22 | var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); 23 | var operator = kw("operator"), atom = {type: "atom", style: "atom"}; 24 | 25 | return { 26 | "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, 27 | "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, 28 | "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), 29 | "function": kw("function"), "catch": kw("catch"), 30 | "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), 31 | "in": operator, "typeof": operator, "instanceof": operator, 32 | "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, 33 | "this": kw("this"), "class": kw("class"), "super": kw("atom"), 34 | "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, 35 | "await": C 36 | }; 37 | }(); 38 | 39 | var isOperatorChar = /[+\-*&%=<>!?|~^@]/; 40 | var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; 41 | 42 | function readRegexp(stream) { 43 | var escaped = false, next, inSet = false; 44 | while ((next = stream.next()) != null) { 45 | if (!escaped) { 46 | if (next == "/" && !inSet) return; 47 | if (next == "[") inSet = true; 48 | else if (inSet && next == "]") inSet = false; 49 | } 50 | escaped = !escaped && next == "\\"; 51 | } 52 | } 53 | 54 | // Used as scratch variables to communicate multiple values without 55 | // consing up tons of objects. 56 | var type, content; 57 | function ret(tp, style, cont) { 58 | type = tp; content = cont; 59 | return style; 60 | } 61 | function tokenBase(stream, state) { 62 | var ch = stream.next(); 63 | if (ch == '"' || ch == "'") { 64 | state.tokenize = tokenString(ch); 65 | return state.tokenize(stream, state); 66 | } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { 67 | return ret("number", "number"); 68 | } else if (ch == "." && stream.match("..")) { 69 | return ret("spread", "meta"); 70 | } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 71 | return ret(ch); 72 | } else if (ch == "=" && stream.eat(">")) { 73 | return ret("=>", "operator"); 74 | } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { 75 | return ret("number", "number"); 76 | } else if (/\d/.test(ch)) { 77 | stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); 78 | return ret("number", "number"); 79 | } else if (ch == "/") { 80 | if (stream.eat("*")) { 81 | state.tokenize = tokenComment; 82 | return tokenComment(stream, state); 83 | } else if (stream.eat("/")) { 84 | stream.skipToEnd(); 85 | return ret("comment", "comment"); 86 | } else if (expressionAllowed(stream, state, 1)) { 87 | readRegexp(stream); 88 | stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); 89 | return ret("regexp", "string-2"); 90 | } else { 91 | stream.eat("="); 92 | return ret("operator", "operator", stream.current()); 93 | } 94 | } else if (ch == "`") { 95 | state.tokenize = tokenQuasi; 96 | return tokenQuasi(stream, state); 97 | } else if (ch == "#" && stream.peek() == "!") { 98 | stream.skipToEnd(); 99 | return ret("meta", "meta"); 100 | } else if (ch == "#" && stream.eatWhile(wordRE)) { 101 | return ret("variable", "property") 102 | } else if (ch == "<" && stream.match("!--") || 103 | (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { 104 | stream.skipToEnd() 105 | return ret("comment", "comment") 106 | } else if (isOperatorChar.test(ch)) { 107 | if (ch != ">" || !state.lexical || state.lexical.type != ">") { 108 | if (stream.eat("=")) { 109 | if (ch == "!" || ch == "=") stream.eat("=") 110 | } else if (/[<>*+\-|&?]/.test(ch)) { 111 | stream.eat(ch) 112 | if (ch == ">") stream.eat(ch) 113 | } 114 | } 115 | if (ch == "?" && stream.eat(".")) return ret(".") 116 | return ret("operator", "operator", stream.current()); 117 | } else if (wordRE.test(ch)) { 118 | stream.eatWhile(wordRE); 119 | var word = stream.current() 120 | if (state.lastType != ".") { 121 | if (keywords.propertyIsEnumerable(word)) { 122 | var kw = keywords[word] 123 | return ret(kw.type, kw.style, word) 124 | } 125 | if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false)) 126 | return ret("async", "keyword", word) 127 | } 128 | return ret("variable", "variable", word) 129 | } 130 | } 131 | 132 | function tokenString(quote) { 133 | return function(stream, state) { 134 | var escaped = false, next; 135 | if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ 136 | state.tokenize = tokenBase; 137 | return ret("jsonld-keyword", "meta"); 138 | } 139 | while ((next = stream.next()) != null) { 140 | if (next == quote && !escaped) break; 141 | escaped = !escaped && next == "\\"; 142 | } 143 | if (!escaped) state.tokenize = tokenBase; 144 | return ret("string", "string"); 145 | }; 146 | } 147 | 148 | function tokenComment(stream, state) { 149 | var maybeEnd = false, ch; 150 | while (ch = stream.next()) { 151 | if (ch == "/" && maybeEnd) { 152 | state.tokenize = tokenBase; 153 | break; 154 | } 155 | maybeEnd = (ch == "*"); 156 | } 157 | return ret("comment", "comment"); 158 | } 159 | 160 | function tokenQuasi(stream, state) { 161 | var escaped = false, next; 162 | while ((next = stream.next()) != null) { 163 | if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { 164 | state.tokenize = tokenBase; 165 | break; 166 | } 167 | escaped = !escaped && next == "\\"; 168 | } 169 | return ret("quasi", "string-2", stream.current()); 170 | } 171 | 172 | var brackets = "([{}])"; 173 | // This is a crude lookahead trick to try and notice that we're 174 | // parsing the argument patterns for a fat-arrow function before we 175 | // actually hit the arrow token. It only works if the arrow is on 176 | // the same line as the arguments and there's no strange noise 177 | // (comments) in between. Fallback is to only notice when we hit the 178 | // arrow, and not declare the arguments as locals for the arrow 179 | // body. 180 | function findFatArrow(stream, state) { 181 | if (state.fatArrowAt) state.fatArrowAt = null; 182 | var arrow = stream.string.indexOf("=>", stream.start); 183 | if (arrow < 0) return; 184 | 185 | if (isTS) { // Try to skip TypeScript return type declarations after the arguments 186 | var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) 187 | if (m) arrow = m.index 188 | } 189 | 190 | var depth = 0, sawSomething = false; 191 | for (var pos = arrow - 1; pos >= 0; --pos) { 192 | var ch = stream.string.charAt(pos); 193 | var bracket = brackets.indexOf(ch); 194 | if (bracket >= 0 && bracket < 3) { 195 | if (!depth) { ++pos; break; } 196 | if (--depth == 0) { if (ch == "(") sawSomething = true; break; } 197 | } else if (bracket >= 3 && bracket < 6) { 198 | ++depth; 199 | } else if (wordRE.test(ch)) { 200 | sawSomething = true; 201 | } else if (/["'\/`]/.test(ch)) { 202 | for (;; --pos) { 203 | if (pos == 0) return 204 | var next = stream.string.charAt(pos - 1) 205 | if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } 206 | } 207 | } else if (sawSomething && !depth) { 208 | ++pos; 209 | break; 210 | } 211 | } 212 | if (sawSomething && !depth) state.fatArrowAt = pos; 213 | } 214 | 215 | // Parser 216 | 217 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; 218 | 219 | function JSLexical(indented, column, type, align, prev, info) { 220 | this.indented = indented; 221 | this.column = column; 222 | this.type = type; 223 | this.prev = prev; 224 | this.info = info; 225 | if (align != null) this.align = align; 226 | } 227 | 228 | function inScope(state, varname) { 229 | for (var v = state.localVars; v; v = v.next) 230 | if (v.name == varname) return true; 231 | for (var cx = state.context; cx; cx = cx.prev) { 232 | for (var v = cx.vars; v; v = v.next) 233 | if (v.name == varname) return true; 234 | } 235 | } 236 | 237 | function parseJS(state, style, type, content, stream) { 238 | var cc = state.cc; 239 | // Communicate our context to the combinators. 240 | // (Less wasteful than consing up a hundred closures on every call.) 241 | cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; 242 | 243 | if (!state.lexical.hasOwnProperty("align")) 244 | state.lexical.align = true; 245 | 246 | while(true) { 247 | var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; 248 | if (combinator(type, content)) { 249 | while(cc.length && cc[cc.length - 1].lex) 250 | cc.pop()(); 251 | if (cx.marked) return cx.marked; 252 | if (type == "variable" && inScope(state, content)) return "variable-2"; 253 | return style; 254 | } 255 | } 256 | } 257 | 258 | // Combinator utils 259 | 260 | var cx = {state: null, column: null, marked: null, cc: null}; 261 | function pass() { 262 | for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 263 | } 264 | function cont() { 265 | pass.apply(null, arguments); 266 | return true; 267 | } 268 | function inList(name, list) { 269 | for (var v = list; v; v = v.next) if (v.name == name) return true 270 | return false; 271 | } 272 | function register(varname) { 273 | var state = cx.state; 274 | cx.marked = "def"; 275 | if (state.context) { 276 | if (state.lexical.info == "var" && state.context && state.context.block) { 277 | 278 | var newContext = registerVarScoped(varname, state.context) 279 | if (newContext != null) { 280 | state.context = newContext 281 | return 282 | } 283 | } else if (!inList(varname, state.localVars)) { 284 | state.localVars = new Var(varname, state.localVars) 285 | return 286 | } 287 | } 288 | // Fall through means this is global 289 | if (parserConfig.globalVars && !inList(varname, state.globalVars)) 290 | state.globalVars = new Var(varname, state.globalVars) 291 | } 292 | function registerVarScoped(varname, context) { 293 | if (!context) { 294 | return null 295 | } else if (context.block) { 296 | var inner = registerVarScoped(varname, context.prev) 297 | if (!inner) return null 298 | if (inner == context.prev) return context 299 | return new Context(inner, context.vars, true) 300 | } else if (inList(varname, context.vars)) { 301 | return context 302 | } else { 303 | return new Context(context.prev, new Var(varname, context.vars), false) 304 | } 305 | } 306 | 307 | function isModifier(name) { 308 | return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" 309 | } 310 | 311 | // Combinators 312 | 313 | function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } 314 | function Var(name, next) { this.name = name; this.next = next } 315 | 316 | var defaultVars = new Var("this", new Var("arguments", null)) 317 | function pushcontext() { 318 | cx.state.context = new Context(cx.state.context, cx.state.localVars, false) 319 | cx.state.localVars = defaultVars 320 | } 321 | function pushblockcontext() { 322 | cx.state.context = new Context(cx.state.context, cx.state.localVars, true) 323 | cx.state.localVars = null 324 | } 325 | function popcontext() { 326 | cx.state.localVars = cx.state.context.vars 327 | cx.state.context = cx.state.context.prev 328 | } 329 | popcontext.lex = true 330 | function pushlex(type, info) { 331 | var result = function() { 332 | var state = cx.state, indent = state.indented; 333 | if (state.lexical.type == "stat") indent = state.lexical.indented; 334 | else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) 335 | indent = outer.indented; 336 | state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); 337 | }; 338 | result.lex = true; 339 | return result; 340 | } 341 | function poplex() { 342 | var state = cx.state; 343 | if (state.lexical.prev) { 344 | if (state.lexical.type == ")") 345 | state.indented = state.lexical.indented; 346 | state.lexical = state.lexical.prev; 347 | } 348 | } 349 | poplex.lex = true; 350 | 351 | function expect(wanted) { 352 | function exp(type) { 353 | if (type == wanted) return cont(); 354 | else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); 355 | else return cont(exp); 356 | }; 357 | return exp; 358 | } 359 | 360 | function statement(type, value) { 361 | if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); 362 | if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); 363 | if (type == "keyword b") return cont(pushlex("form"), statement, poplex); 364 | if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); 365 | if (type == "debugger") return cont(expect(";")); 366 | if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); 367 | if (type == ";") return cont(); 368 | if (type == "if") { 369 | if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) 370 | cx.state.cc.pop()(); 371 | return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); 372 | } 373 | if (type == "function") return cont(functiondef); 374 | if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); 375 | if (type == "class" || (isTS && value == "interface")) { 376 | cx.marked = "keyword" 377 | return cont(pushlex("form", type == "class" ? type : value), className, poplex) 378 | } 379 | if (type == "variable") { 380 | if (isTS && value == "declare") { 381 | cx.marked = "keyword" 382 | return cont(statement) 383 | } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { 384 | cx.marked = "keyword" 385 | if (value == "enum") return cont(enumdef); 386 | else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); 387 | else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) 388 | } else if (isTS && value == "namespace") { 389 | cx.marked = "keyword" 390 | return cont(pushlex("form"), expression, statement, poplex) 391 | } else if (isTS && value == "abstract") { 392 | cx.marked = "keyword" 393 | return cont(statement) 394 | } else { 395 | return cont(pushlex("stat"), maybelabel); 396 | } 397 | } 398 | if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, 399 | block, poplex, poplex, popcontext); 400 | if (type == "case") return cont(expression, expect(":")); 401 | if (type == "default") return cont(expect(":")); 402 | if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); 403 | if (type == "export") return cont(pushlex("stat"), afterExport, poplex); 404 | if (type == "import") return cont(pushlex("stat"), afterImport, poplex); 405 | if (type == "async") return cont(statement) 406 | if (value == "@") return cont(expression, statement) 407 | return pass(pushlex("stat"), expression, expect(";"), poplex); 408 | } 409 | function maybeCatchBinding(type) { 410 | if (type == "(") return cont(funarg, expect(")")) 411 | } 412 | function expression(type, value) { 413 | return expressionInner(type, value, false); 414 | } 415 | function expressionNoComma(type, value) { 416 | return expressionInner(type, value, true); 417 | } 418 | function parenExpr(type) { 419 | if (type != "(") return pass() 420 | return cont(pushlex(")"), maybeexpression, expect(")"), poplex) 421 | } 422 | function expressionInner(type, value, noComma) { 423 | if (cx.state.fatArrowAt == cx.stream.start) { 424 | var body = noComma ? arrowBodyNoComma : arrowBody; 425 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); 426 | else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); 427 | } 428 | 429 | var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; 430 | if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); 431 | if (type == "function") return cont(functiondef, maybeop); 432 | if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } 433 | if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); 434 | if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); 435 | if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); 436 | if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); 437 | if (type == "{") return contCommasep(objprop, "}", null, maybeop); 438 | if (type == "quasi") return pass(quasi, maybeop); 439 | if (type == "new") return cont(maybeTarget(noComma)); 440 | if (type == "import") return cont(expression); 441 | return cont(); 442 | } 443 | function maybeexpression(type) { 444 | if (type.match(/[;\}\)\],]/)) return pass(); 445 | return pass(expression); 446 | } 447 | 448 | function maybeoperatorComma(type, value) { 449 | if (type == ",") return cont(maybeexpression); 450 | return maybeoperatorNoComma(type, value, false); 451 | } 452 | function maybeoperatorNoComma(type, value, noComma) { 453 | var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; 454 | var expr = noComma == false ? expression : expressionNoComma; 455 | if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); 456 | if (type == "operator") { 457 | if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); 458 | if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) 459 | return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); 460 | if (value == "?") return cont(expression, expect(":"), expr); 461 | return cont(expr); 462 | } 463 | if (type == "quasi") { return pass(quasi, me); } 464 | if (type == ";") return; 465 | if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); 466 | if (type == ".") return cont(property, me); 467 | if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); 468 | if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } 469 | if (type == "regexp") { 470 | cx.state.lastType = cx.marked = "operator" 471 | cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) 472 | return cont(expr) 473 | } 474 | } 475 | function quasi(type, value) { 476 | if (type != "quasi") return pass(); 477 | if (value.slice(value.length - 2) != "${") return cont(quasi); 478 | return cont(expression, continueQuasi); 479 | } 480 | function continueQuasi(type) { 481 | if (type == "}") { 482 | cx.marked = "string-2"; 483 | cx.state.tokenize = tokenQuasi; 484 | return cont(quasi); 485 | } 486 | } 487 | function arrowBody(type) { 488 | findFatArrow(cx.stream, cx.state); 489 | return pass(type == "{" ? statement : expression); 490 | } 491 | function arrowBodyNoComma(type) { 492 | findFatArrow(cx.stream, cx.state); 493 | return pass(type == "{" ? statement : expressionNoComma); 494 | } 495 | function maybeTarget(noComma) { 496 | return function(type) { 497 | if (type == ".") return cont(noComma ? targetNoComma : target); 498 | else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) 499 | else return pass(noComma ? expressionNoComma : expression); 500 | }; 501 | } 502 | function target(_, value) { 503 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } 504 | } 505 | function targetNoComma(_, value) { 506 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } 507 | } 508 | function maybelabel(type) { 509 | if (type == ":") return cont(poplex, statement); 510 | return pass(maybeoperatorComma, expect(";"), poplex); 511 | } 512 | function property(type) { 513 | if (type == "variable") {cx.marked = "property"; return cont();} 514 | } 515 | function objprop(type, value) { 516 | if (type == "async") { 517 | cx.marked = "property"; 518 | return cont(objprop); 519 | } else if (type == "variable" || cx.style == "keyword") { 520 | cx.marked = "property"; 521 | if (value == "get" || value == "set") return cont(getterSetter); 522 | var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params 523 | if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) 524 | cx.state.fatArrowAt = cx.stream.pos + m[0].length 525 | return cont(afterprop); 526 | } else if (type == "number" || type == "string") { 527 | cx.marked = jsonldMode ? "property" : (cx.style + " property"); 528 | return cont(afterprop); 529 | } else if (type == "jsonld-keyword") { 530 | return cont(afterprop); 531 | } else if (isTS && isModifier(value)) { 532 | cx.marked = "keyword" 533 | return cont(objprop) 534 | } else if (type == "[") { 535 | return cont(expression, maybetype, expect("]"), afterprop); 536 | } else if (type == "spread") { 537 | return cont(expressionNoComma, afterprop); 538 | } else if (value == "*") { 539 | cx.marked = "keyword"; 540 | return cont(objprop); 541 | } else if (type == ":") { 542 | return pass(afterprop) 543 | } 544 | } 545 | function getterSetter(type) { 546 | if (type != "variable") return pass(afterprop); 547 | cx.marked = "property"; 548 | return cont(functiondef); 549 | } 550 | function afterprop(type) { 551 | if (type == ":") return cont(expressionNoComma); 552 | if (type == "(") return pass(functiondef); 553 | } 554 | function commasep(what, end, sep) { 555 | function proceed(type, value) { 556 | if (sep ? sep.indexOf(type) > -1 : type == ",") { 557 | var lex = cx.state.lexical; 558 | if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; 559 | return cont(function(type, value) { 560 | if (type == end || value == end) return pass() 561 | return pass(what) 562 | }, proceed); 563 | } 564 | if (type == end || value == end) return cont(); 565 | if (sep && sep.indexOf(";") > -1) return pass(what) 566 | return cont(expect(end)); 567 | } 568 | return function(type, value) { 569 | if (type == end || value == end) return cont(); 570 | return pass(what, proceed); 571 | }; 572 | } 573 | function contCommasep(what, end, info) { 574 | for (var i = 3; i < arguments.length; i++) 575 | cx.cc.push(arguments[i]); 576 | return cont(pushlex(end, info), commasep(what, end), poplex); 577 | } 578 | function block(type) { 579 | if (type == "}") return cont(); 580 | return pass(statement, block); 581 | } 582 | function maybetype(type, value) { 583 | if (isTS) { 584 | if (type == ":") return cont(typeexpr); 585 | if (value == "?") return cont(maybetype); 586 | } 587 | } 588 | function maybetypeOrIn(type, value) { 589 | if (isTS && (type == ":" || value == "in")) return cont(typeexpr) 590 | } 591 | function mayberettype(type) { 592 | if (isTS && type == ":") { 593 | if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) 594 | else return cont(typeexpr) 595 | } 596 | } 597 | function isKW(_, value) { 598 | if (value == "is") { 599 | cx.marked = "keyword" 600 | return cont() 601 | } 602 | } 603 | function typeexpr(type, value) { 604 | if (value == "keyof" || value == "typeof" || value == "infer") { 605 | cx.marked = "keyword" 606 | return cont(value == "typeof" ? expressionNoComma : typeexpr) 607 | } 608 | if (type == "variable" || value == "void") { 609 | cx.marked = "type" 610 | return cont(afterType) 611 | } 612 | if (value == "|" || value == "&") return cont(typeexpr) 613 | if (type == "string" || type == "number" || type == "atom") return cont(afterType); 614 | if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) 615 | if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) 616 | if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) 617 | if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) 618 | } 619 | function maybeReturnType(type) { 620 | if (type == "=>") return cont(typeexpr) 621 | } 622 | function typeprop(type, value) { 623 | if (type == "variable" || cx.style == "keyword") { 624 | cx.marked = "property" 625 | return cont(typeprop) 626 | } else if (value == "?" || type == "number" || type == "string") { 627 | return cont(typeprop) 628 | } else if (type == ":") { 629 | return cont(typeexpr) 630 | } else if (type == "[") { 631 | return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) 632 | } else if (type == "(") { 633 | return pass(functiondecl, typeprop) 634 | } 635 | } 636 | function typearg(type, value) { 637 | if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) 638 | if (type == ":") return cont(typeexpr) 639 | if (type == "spread") return cont(typearg) 640 | return pass(typeexpr) 641 | } 642 | function afterType(type, value) { 643 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 644 | if (value == "|" || type == "." || value == "&") return cont(typeexpr) 645 | if (type == "[") return cont(typeexpr, expect("]"), afterType) 646 | if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } 647 | if (value == "?") return cont(typeexpr, expect(":"), typeexpr) 648 | } 649 | function maybeTypeArgs(_, value) { 650 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 651 | } 652 | function typeparam() { 653 | return pass(typeexpr, maybeTypeDefault) 654 | } 655 | function maybeTypeDefault(_, value) { 656 | if (value == "=") return cont(typeexpr) 657 | } 658 | function vardef(_, value) { 659 | if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} 660 | return pass(pattern, maybetype, maybeAssign, vardefCont); 661 | } 662 | function pattern(type, value) { 663 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } 664 | if (type == "variable") { register(value); return cont(); } 665 | if (type == "spread") return cont(pattern); 666 | if (type == "[") return contCommasep(eltpattern, "]"); 667 | if (type == "{") return contCommasep(proppattern, "}"); 668 | } 669 | function proppattern(type, value) { 670 | if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { 671 | register(value); 672 | return cont(maybeAssign); 673 | } 674 | if (type == "variable") cx.marked = "property"; 675 | if (type == "spread") return cont(pattern); 676 | if (type == "}") return pass(); 677 | if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); 678 | return cont(expect(":"), pattern, maybeAssign); 679 | } 680 | function eltpattern() { 681 | return pass(pattern, maybeAssign) 682 | } 683 | function maybeAssign(_type, value) { 684 | if (value == "=") return cont(expressionNoComma); 685 | } 686 | function vardefCont(type) { 687 | if (type == ",") return cont(vardef); 688 | } 689 | function maybeelse(type, value) { 690 | if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); 691 | } 692 | function forspec(type, value) { 693 | if (value == "await") return cont(forspec); 694 | if (type == "(") return cont(pushlex(")"), forspec1, poplex); 695 | } 696 | function forspec1(type) { 697 | if (type == "var") return cont(vardef, forspec2); 698 | if (type == "variable") return cont(forspec2); 699 | return pass(forspec2) 700 | } 701 | function forspec2(type, value) { 702 | if (type == ")") return cont() 703 | if (type == ";") return cont(forspec2) 704 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } 705 | return pass(expression, forspec2) 706 | } 707 | function functiondef(type, value) { 708 | if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} 709 | if (type == "variable") {register(value); return cont(functiondef);} 710 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); 711 | if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) 712 | } 713 | function functiondecl(type, value) { 714 | if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} 715 | if (type == "variable") {register(value); return cont(functiondecl);} 716 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); 717 | if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) 718 | } 719 | function typename(type, value) { 720 | if (type == "keyword" || type == "variable") { 721 | cx.marked = "type" 722 | return cont(typename) 723 | } else if (value == "<") { 724 | return cont(pushlex(">"), commasep(typeparam, ">"), poplex) 725 | } 726 | } 727 | function funarg(type, value) { 728 | if (value == "@") cont(expression, funarg) 729 | if (type == "spread") return cont(funarg); 730 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } 731 | if (isTS && type == "this") return cont(maybetype, maybeAssign) 732 | return pass(pattern, maybetype, maybeAssign); 733 | } 734 | function classExpression(type, value) { 735 | // Class expressions may have an optional name. 736 | if (type == "variable") return className(type, value); 737 | return classNameAfter(type, value); 738 | } 739 | function className(type, value) { 740 | if (type == "variable") {register(value); return cont(classNameAfter);} 741 | } 742 | function classNameAfter(type, value) { 743 | if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) 744 | if (value == "extends" || value == "implements" || (isTS && type == ",")) { 745 | if (value == "implements") cx.marked = "keyword"; 746 | return cont(isTS ? typeexpr : expression, classNameAfter); 747 | } 748 | if (type == "{") return cont(pushlex("}"), classBody, poplex); 749 | } 750 | function classBody(type, value) { 751 | if (type == "async" || 752 | (type == "variable" && 753 | (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && 754 | cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { 755 | cx.marked = "keyword"; 756 | return cont(classBody); 757 | } 758 | if (type == "variable" || cx.style == "keyword") { 759 | cx.marked = "property"; 760 | return cont(classfield, classBody); 761 | } 762 | if (type == "number" || type == "string") return cont(classfield, classBody); 763 | if (type == "[") 764 | return cont(expression, maybetype, expect("]"), classfield, classBody) 765 | if (value == "*") { 766 | cx.marked = "keyword"; 767 | return cont(classBody); 768 | } 769 | if (isTS && type == "(") return pass(functiondecl, classBody) 770 | if (type == ";" || type == ",") return cont(classBody); 771 | if (type == "}") return cont(); 772 | if (value == "@") return cont(expression, classBody) 773 | } 774 | function classfield(type, value) { 775 | if (value == "?") return cont(classfield) 776 | if (type == ":") return cont(typeexpr, maybeAssign) 777 | if (value == "=") return cont(expressionNoComma) 778 | var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" 779 | return pass(isInterface ? functiondecl : functiondef) 780 | } 781 | function afterExport(type, value) { 782 | if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } 783 | if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } 784 | if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); 785 | return pass(statement); 786 | } 787 | function exportField(type, value) { 788 | if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } 789 | if (type == "variable") return pass(expressionNoComma, exportField); 790 | } 791 | function afterImport(type) { 792 | if (type == "string") return cont(); 793 | if (type == "(") return pass(expression); 794 | return pass(importSpec, maybeMoreImports, maybeFrom); 795 | } 796 | function importSpec(type, value) { 797 | if (type == "{") return contCommasep(importSpec, "}"); 798 | if (type == "variable") register(value); 799 | if (value == "*") cx.marked = "keyword"; 800 | return cont(maybeAs); 801 | } 802 | function maybeMoreImports(type) { 803 | if (type == ",") return cont(importSpec, maybeMoreImports) 804 | } 805 | function maybeAs(_type, value) { 806 | if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } 807 | } 808 | function maybeFrom(_type, value) { 809 | if (value == "from") { cx.marked = "keyword"; return cont(expression); } 810 | } 811 | function arrayLiteral(type) { 812 | if (type == "]") return cont(); 813 | return pass(commasep(expressionNoComma, "]")); 814 | } 815 | function enumdef() { 816 | return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) 817 | } 818 | function enummember() { 819 | return pass(pattern, maybeAssign); 820 | } 821 | 822 | function isContinuedStatement(state, textAfter) { 823 | return state.lastType == "operator" || state.lastType == "," || 824 | isOperatorChar.test(textAfter.charAt(0)) || 825 | /[,.]/.test(textAfter.charAt(0)); 826 | } 827 | 828 | function expressionAllowed(stream, state, backUp) { 829 | return state.tokenize == tokenBase && 830 | /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || 831 | (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) 832 | } 833 | 834 | // Interface 835 | 836 | return { 837 | startState: function(basecolumn) { 838 | var state = { 839 | tokenize: tokenBase, 840 | lastType: "sof", 841 | cc: [], 842 | lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), 843 | localVars: parserConfig.localVars, 844 | context: parserConfig.localVars && new Context(null, null, false), 845 | indented: basecolumn || 0 846 | }; 847 | if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") 848 | state.globalVars = parserConfig.globalVars; 849 | return state; 850 | }, 851 | 852 | token: function(stream, state) { 853 | if (stream.sol()) { 854 | if (!state.lexical.hasOwnProperty("align")) 855 | state.lexical.align = false; 856 | state.indented = stream.indentation(); 857 | findFatArrow(stream, state); 858 | } 859 | if (state.tokenize != tokenComment && stream.eatSpace()) return null; 860 | var style = state.tokenize(stream, state); 861 | if (type == "comment") return style; 862 | state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; 863 | return parseJS(state, style, type, content, stream); 864 | }, 865 | 866 | indent: function(state, textAfter) { 867 | if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass; 868 | if (state.tokenize != tokenBase) return 0; 869 | var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top 870 | // Kludge to prevent 'maybelse' from blocking lexical scope pops 871 | if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { 872 | var c = state.cc[i]; 873 | if (c == poplex) lexical = lexical.prev; 874 | else if (c != maybeelse) break; 875 | } 876 | while ((lexical.type == "stat" || lexical.type == "form") && 877 | (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && 878 | (top == maybeoperatorComma || top == maybeoperatorNoComma) && 879 | !/^[,\.=+\-*:?[\(]/.test(textAfter)))) 880 | lexical = lexical.prev; 881 | if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") 882 | lexical = lexical.prev; 883 | var type = lexical.type, closing = firstChar == type; 884 | 885 | if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); 886 | else if (type == "form" && firstChar == "{") return lexical.indented; 887 | else if (type == "form") return lexical.indented + indentUnit; 888 | else if (type == "stat") 889 | return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); 890 | else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) 891 | return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); 892 | else if (lexical.align) return lexical.column + (closing ? 0 : 1); 893 | else return lexical.indented + (closing ? 0 : indentUnit); 894 | }, 895 | 896 | electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, 897 | blockCommentStart: jsonMode ? null : "/*", 898 | blockCommentEnd: jsonMode ? null : "*/", 899 | blockCommentContinue: jsonMode ? null : " * ", 900 | lineComment: jsonMode ? null : "//", 901 | fold: "brace", 902 | closeBrackets: "()[]{}''\"\"``", 903 | 904 | helperType: jsonMode ? "json" : "javascript", 905 | jsonldMode: jsonldMode, 906 | jsonMode: jsonMode, 907 | 908 | expressionAllowed: expressionAllowed, 909 | 910 | skipExpression: function(state) { 911 | var top = state.cc[state.cc.length - 1] 912 | if (top == expression || top == expressionNoComma) state.cc.pop() 913 | } 914 | }; 915 | }); 916 | 917 | // CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); 918 | 919 | CodeMirror.defineMIME("text/smart-gantt-list", "gantt"); 920 | 921 | }); 922 | --------------------------------------------------------------------------------