Compare commits

...

8 commits

Author SHA1 Message Date
Anton Duda
ee659c57e9 chore: revert version to 0.0.3 2026-06-01 10:41:46 +03:00
Anton Duda
3f82b5313b 0.0.4
update
2026-05-31 23:31:23 +03:00
Anton Duda
54f19119e9 ci: enable legacy-peer-deps for obsidian/codemirror conflict 2026-05-31 23:30:10 +03:00
Anton Duda
9196e6c476 0.0.3
v0.0.3
2026-05-31 23:23:02 +03:00
Anton Duda
56d8b189c8 chore(lint): migrate to eslint 10 + eslint-plugin-obsidianmd 2026-05-31 23:19:08 +03:00
Anton Duda
4349874719 fix(vueUtils): restore needed genid 2025-11-26 01:57:24 +03:00
Anton Duda
02955a1a2f refactor: Address linter warnings 2025-11-26 01:46:35 +03:00
Anton Duda
d6f8b29164 feat: Activate insert strudel block command 2025-11-23 19:16:05 +03:00
17 changed files with 4061 additions and 963 deletions

View file

@ -1,3 +0,0 @@
node_modules/
main.js

View file

@ -1,84 +0,0 @@
{
"parser": "vue-eslint-parser",
"plugins": ["@typescript-eslint/eslint-plugin", "eslint-plugin-vue", "prettier"],
"extends": ["plugin:@typescript-eslint/recommended", "plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"node": true
},
"ignorePatterns": [
"docker-formatter.*",
"node_modules/*",
".nuxt/*",
"dist/*",
"dist-electron/*",
"server/*",
"scripts/*",
"src/assets/*"
],
"rules": {
"arrow-body-style": 0,
"class-methods-use-this": 0,
"comma-dangle": 0,
"func-names": 0,
"import/extensions": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default-member": 0,
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"linebreak-style": 0,
"max-len": [
"warn",
{
"code": 120,
"ignoreTemplateLiterals": true,
"ignoreComments": true,
"ignoreStrings": true
}
],
"no-await-in-loop": 0,
"no-console": 0,
"no-loop-func": 0,
"no-mixed-operators": 0,
"no-param-reassign": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-trailing-spaces": 1,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"prefer-destructuring": 0,
"semi": 0,
"space-before-function-paren": 0,
"vue/multi-word-component-names": "off",
"vue/no-dupe-keys": "warn",
"vue/no-mutating-props": ["warn", { "shallowOnly": true }],
"vue/no-unused-vars": ["warn", { "ignorePattern": "^_" }],
"vue/no-use-v-if-with-v-for": "warn",
"vue/prop-name-casing": "error",
"vue/return-in-computed-property": "off",
"vue/require-prop-types": "error",
"vue/require-toggle-inside-transition": "warn",
"vue/require-typed-object-prop": "error",
"vue/require-v-for-key": "warn",
"vue/valid-v-for": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-inferrable-types": ["warn", { "ignoreParameters": true }],
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"prettier/prettier": ["warn"]
}
}

3
.npmrc
View file

@ -1 +1,2 @@
tag-version-prefix=""
tag-version-prefix=""
legacy-peer-deps=true

149
eslint.config.mjs Normal file
View file

@ -0,0 +1,149 @@
import obsidianmd from "eslint-plugin-obsidianmd";
import vue from "eslint-plugin-vue";
import tseslint from "typescript-eslint";
import vueParser from "vue-eslint-parser";
import prettierRecommended from "eslint-plugin-prettier/recommended";
export default [
{
ignores: [
"node_modules/**",
"build/**",
"dist/**",
"main.js",
"src/strudel/**",
"src/assets/**",
"docker-formatter.*",
],
},
...obsidianmd.configs.recommended,
// Workaround: obsidianmd recommended preset declares the typed rule
// `no-plugin-as-component` globally, which crashes on non-TS files
// (it calls getParserServices). Disable it for everything except TS.
{
files: ["**/*.json", "**/*.vue", "**/*.js", "**/*.mjs", "**/*.cjs"],
rules: {
"obsidianmd/no-plugin-as-component": "off",
},
},
// Preserve product/acronym casing in UI strings. `ignoreWords` is
// case-sensitive, so list every form that appears in source.
{
rules: {
"obsidianmd/ui/sentence-case": [
"error",
{
enforceCamelCaseLower: true,
ignoreWords: ["Strudel", "strudel", "REPL", "repl", "URLs", "urls", "URL", "url"],
},
],
},
},
...vue.configs["flat/recommended"],
{
files: ["**/*.vue"],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tseslint.parser,
ecmaVersion: "latest",
sourceType: "module",
extraFileExtensions: [".vue"],
},
},
},
{
files: ["src/**/*.ts", "src/**/*.tsx"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
prettierRecommended,
// Generic rules — apply to all linted files
{
rules: {
"arrow-body-style": 0,
"class-methods-use-this": 0,
"comma-dangle": 0,
"func-names": 0,
"linebreak-style": 0,
"max-len": [
"warn",
{
code: 120,
ignoreTemplateLiterals: true,
ignoreComments: true,
ignoreStrings: true,
},
],
"no-await-in-loop": 0,
"no-console": 0,
"no-loop-func": 0,
"no-mixed-operators": 0,
"no-param-reassign": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-trailing-spaces": 1,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"prefer-destructuring": 0,
semi: 0,
"space-before-function-paren": 0,
"prettier/prettier": ["warn"],
},
},
// Vue rules — apply only to .vue files
{
files: ["**/*.vue"],
rules: {
"vue/multi-word-component-names": "off",
"vue/no-dupe-keys": "warn",
"vue/no-mutating-props": ["warn", { shallowOnly: true }],
"vue/no-unused-vars": ["warn", { ignorePattern: "^_" }],
"vue/no-use-v-if-with-v-for": "warn",
"vue/prop-name-casing": "error",
"vue/return-in-computed-property": "off",
"vue/require-prop-types": "error",
"vue/require-toggle-inside-transition": "warn",
"vue/require-typed-object-prop": "error",
"vue/require-v-for-key": "warn",
"vue/valid-v-for": "warn",
},
},
// TS rules — apply only where @typescript-eslint plugin is registered
{
files: ["**/*.ts", "**/*.tsx"],
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-inferrable-types": ["warn", { ignoreParameters: true }],
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
},
];

View file

@ -1,7 +1,7 @@
{
"id": "strudel-repl",
"name": "Strudel REPL",
"version": "0.0.2",
"version": "0.0.3",
"minAppVersion": "1.9.14",
"description": "Live-coding music environment — Strudel REPL integrated into your vault.",
"author": "Anton Duda",

4582
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,12 @@
{
"name": "strudel-obsidian-plugin",
"version": "0.0.2",
"version": "0.0.3",
"description": "Strudel REPL Obsidian plugin",
"main": "main.js",
"scripts": {
"dev": "vite preview",
"build": "tsc -noEmit -skipLibCheck && vite build",
"lint": "eslint \"src/**/*.{ts,tsx,vue}\"",
"deploy:test": "bash ./build_and_copy_plugin.sh",
"deploy:prod": "bash ./build_and_copy_plugin.sh prod",
"version": "node version-bump.mjs && git add manifest.json versions.json"
@ -18,28 +19,34 @@
"@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.6.2",
"@types/node": "^24.9.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.60.0",
"@vitejs/plugin-vue": "^5.2.4",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild-plugin-vue3": "^0.3.2",
"eslint": "8.31.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-vue": "9.23.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-vue": "^10.9.1",
"globals": "^17.6.0",
"obsidian": "latest",
"prettier": "^3.5.3",
"prettier-eslint": "16.1.2",
"sass-embedded": "^1.93.2",
"tslib": "2.4.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.60.0",
"vite": "^6.3.5",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^2.2.10"
},
"dependencies": {
"@codemirror/language": "https://github.com/lishid/cm-language",
"@codemirror/merge": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.8",
"@internationalized/date": "^3.8.2",
"@tonaljs/tonal": "^4.10.0",
"@vueuse/core": "^13.9.0",

View file

@ -1,9 +1,9 @@
import { STRUDEL_CODEBLOCK_KEYWORD } from '@/constants/keywords'
import { Editor, Notice } from 'obsidian'
export const createStrudelBlock = async (editor: Editor) => {
export const createStrudelBlock = (editor: Editor) => {
if (!editor) {
new Notice('No active markdown editor found.', 3000)
new Notice('No active Markdown editor found.', 3000)
return
}
@ -15,7 +15,6 @@ export const createStrudelBlock = async (editor: Editor) => {
\`\`\``
if (selection) {
editor.replaceSelection(strudelBlock)
} else {

View file

@ -16,7 +16,7 @@ export class StrudelHeaderWidget extends WidgetType {
}
toDOM() {
const container = document.createElement('div')
const container = activeDocument.createElement('div')
container.id = this.strudelBlock.id
container.createDiv({ attr: { 'data-strudel-id': this.strudelBlock.id } })
@ -29,7 +29,7 @@ export class StrudelHeaderWidget extends WidgetType {
// deleting the container from the DOM. This means Vue doesn't have enough time
// to unmount the component from the old container.
// To avoid this issue, we render the block with a slight delay.
setTimeout(() => {
window.setTimeout(() => {
this.strudelBlock.show()
})

View file

@ -1,4 +1,4 @@
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state'
import { Decoration, DecorationSet, EditorView } from '@codemirror/view'
export const setMiniLocations = StateEffect.define<{
@ -110,7 +110,7 @@ const miniLocationHighlights = EditorView.decorations.compute(
const colorMod = hap.value?.color ? ` strudel-mark_${hap.value.color}` : ''
const classStr = `strudel-mark${colorMod}`
// Get explicit channels for color values
/*
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
@ -142,4 +142,3 @@ const miniLocationHighlights = EditorView.decorations.compute(
)
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]

View file

@ -1,115 +1,4 @@
import { nanoid } from 'nanoid'
import { App } from 'obsidian'
import { createPinia } from 'pinia'
import { createApp, defineComponent, Component as VueComponent } from 'vue'
/**
* Helper class for managing Vue components in Obsidian
*/
export class VueRenderer {
private static instance: VueRenderer
private app: App
private vueApps: Map<string, any> = new Map()
private constructor(app: App) {
this.app = app
}
/**
* Get the singleton instance of VueRenderer
* @param app Obsidian App instance
* @returns VueRenderer instance
*/
public static getInstance(app: App): VueRenderer {
if (!VueRenderer.instance) {
VueRenderer.instance = new VueRenderer(app)
}
return VueRenderer.instance
}
/**
* Mount a Vue component to a DOM element
* @param container The DOM element to mount the component to
* @param component The Vue component to mount
* @param props Props to pass to the component
* @returns A unique ID for the mounted component
*/
public mountComponent(
container: HTMLElement,
component: VueComponent,
props: Record<string, any> = {}
): string {
// Create a unique ID for this component instance
const id = `vue-component-${Date.now()}-${Math.floor(Math.random() * 10000)}`
// Create a wrapper div for the Vue app
const mountPoint = document.createElement('div')
mountPoint.id = id
container.appendChild(mountPoint)
const pinia = createPinia()
// Create a Vue app with the component
const app = createApp(component, props)
app.use(pinia)
// Mount the app to the container
app.mount(`#${id}`)
// Store the app reference for cleanup
this.vueApps.set(id, app)
return id
}
/**
* Unmount a Vue component by its ID
* @param id The ID of the component to unmount
*/
public unmountComponent(id: string): void {
const app = this.vueApps.get(id)
if (app) {
app.unmount()
this.vueApps.delete(id)
const mountPoint = document.getElementById(id)
if (mountPoint) {
mountPoint.remove()
}
}
}
/**
* Create a wrapper for a Vue component that can be used with Obsidian's Component system
* @param component The Vue component to wrap
* @param props Props to pass to the component
* @returns A function that can be used to mount the component
*/
public createComponentWrapper(
component: VueComponent,
props: Record<string, any> = {}
): (container: HTMLElement) => string {
return (container: HTMLElement) => {
return this.mountComponent(container, component, props)
}
}
}
/**
* Create a simple Vue component from a template string
* @param template The Vue template string
* @param options Additional Vue component options
* @returns A Vue component
*/
export function createVueComponent(
template: string,
options: Record<string, any> = {}
): VueComponent {
return defineComponent({
template,
...options,
})
}
export function genid(): string {
return `vue-${nanoid()}`

View file

@ -12,18 +12,15 @@ import { samples } from '@/strudel/init.js'
import './editor/SyntaxHighlighting.js'
import { createStrudelBlock } from './commands/createStrudelBlock'
interface PluginData {}
export default class StrudelPlugin extends Plugin {
strudelConfig: StrudelConfig
private data: PluginData = {}
private vueApp: VueApp | null = null
initializeVue() {
const rootContainer = document.createElement('div')
const rootContainer = activeDocument.createElement('div')
rootContainer.id = 'strudel-vue-root'
document.body.appendChild(rootContainer)
activeDocument.body.appendChild(rootContainer)
this.vueApp = createApp(VueEntry)
this.vueApp.use(createPinia())
@ -41,18 +38,18 @@ export default class StrudelPlugin extends Plugin {
this.addSettingTab(new StrudelSettingTab(this.app, this))
console.log('Strudel REPL Plugin loaded.')
console.debug('Strudel REPL plugin loaded.')
this.initializeVue()
this.registerEditorExtension(strudelEditorExtension)
this.registerEditorExtension(highlightExtension)
GlobalStore.getInstance().initStrudel()
void GlobalStore.getInstance().initStrudel()
this.addCommand({
id: 'play',
name: 'Play strudel block at cursor',
name: 'Play strudel block at caret',
editorCallback: (editor: Editor) => {
const cursor = editor.getCursor()
@ -68,22 +65,22 @@ export default class StrudelPlugin extends Plugin {
},
})
// this.addCommand({
// id: 'create-strudel-block',
// name: 'Create new strudel block',
// callback: () => {
// createStrudelBlock()
// },
// })
this.addCommand({
id: 'create-strudel-block',
name: 'Create new Strudel block',
editorCallback: (editor: Editor) => {
createStrudelBlock(editor)
},
})
}
onunload() {
GlobalStore.getInstance().destroy()
if (this.vueApp) {
this.vueApp.unmount()
document.getElementById('strudel-vue-root')?.remove()
activeDocument.getElementById('strudel-vue-root')?.remove()
}
console.log('Strudel REPL plugin unloaded.')
console.debug('Strudel REPL plugin unloaded.')
}
async loadSettings() {
@ -109,8 +106,6 @@ class StrudelSettingTab extends PluginSettingTab {
containerEl.empty()
containerEl.createEl('h2', { text: 'Strudel REPL Settings' })
new Setting(containerEl)
.setName('Sounds cache directory')
.setDesc('Directory where Strudel will store cached sounds.')
@ -138,7 +133,7 @@ class StrudelSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Samples to preload')
.setDesc('URLs of audio samples to preload on startup for faster access.')
.setDesc('Audio sample urls to preload on startup for faster access.')
.addTextArea((text) =>
text
.setPlaceholder('Enter sample URLs, one per line')

View file

@ -35,7 +35,7 @@ export class CacheService {
const data = JSON.parse(content)
return data
}
} catch (error) {
} catch {
// ignore error
}
}
@ -63,6 +63,6 @@ export class CacheService {
}
escapeFileName(fileName: string): string {
return fileName.replace(/[^a-z0-9_\-\.]/gi, '_')
return fileName.replace(/[^a-z0-9_\-.]/gi, '_')
}
}

View file

@ -22,8 +22,9 @@ export class StrudelConfig {
public saveToCache = true
public samplesToPreload: string[] = [...defaultSamples]
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
private constructor() {
return
}
public static getInstance(): StrudelConfig {
if (!StrudelConfig.instance) {

View file

@ -41,7 +41,7 @@ export class GlobalStore {
const view = activeLeaf?.view as MarkdownView
if (activeLeaf && view.editor) {
return (view.editor as Editor & { cm: EditorView }).cm as EditorView
return (view.editor as Editor & { cm: EditorView }).cm
}
}
@ -73,7 +73,7 @@ export class GlobalStore {
this.isPlaying.value = false
}
public drawer: any | null = null
public drawer: any = null
public async initStrudel() {
this.repl = await init({
@ -119,7 +119,7 @@ export class GlobalStore {
const currentFrame = haps.filter((hap: any) => hap.isActive(time))
highlightMiniLocations(editor, time, currentFrame)
const rootStyles = getComputedStyle(document.getElementsByTagName('body')[0])
const rootStyles = getComputedStyle(activeDocument.getElementsByTagName('body')[0])
const playheadColor = rootStyles.getPropertyValue('--text-accent').trim()
const inactiveColor = rootStyles
.getPropertyValue('--background-modifier-active-hover')

4
src/vue-shims.d.ts vendored
View file

@ -1,7 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, any>
export default component
}

View file

@ -1,4 +1,5 @@
{
"0.0.1": "1.9.14",
"0.0.2": "1.9.14",
"0.0.3": "1.9.14",
}