Merge branch 'main' into qi

This commit is contained in:
2022-04-09 13:49:37 -05:00
20 changed files with 1023 additions and 18 deletions

View File

@@ -1,10 +1,38 @@
<script setup lang="ts">
import Home from './components/Home.vue'
import Home from "./pages/Login.vue";
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from "./components/HelloWorld.vue";
import { MathStatement } from "./support/parse";
import { MathPage } from "./support/page";
import { ref } from "vue";
(window as any).Stmt = MathStatement;
(window as any).Pg = MathPage;
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<Home msg="CrystalMathy" />
<q-layout view="hHr LpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg" />
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/Scratch" label="Scratch" />
<q-route-tab to="/Editor" label="Editor" />
</q-tabs>
</q-header>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style>

40
src/assets/edit.svg Normal file
View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 612.097 612.097" style="enable-background:new 0 0 612.097 612.097;" xml:space="preserve">
<g>
<path d="M320.85,151.725l139.6,139.6l-238,237.9l-139.6-139.6L320.85,151.725z M607.65,124.125l-119.7-119.7
c-5.9-5.9-15.5-5.9-21.4,0l-104,104l141,141l104-104C613.55,139.525,613.55,130.025,607.65,124.125z M0.55,592.825
c-1.4,5.3,0.1,10.9,3.9,14.8c3.9,3.9,9.5,5.4,14.8,3.9l165.5-44.6l-139.6-139.6L0.55,592.825z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

30
src/components/Katex.vue Normal file
View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import {computed} from 'vue'
import {MathStatement} from '../support/parse'
const props = defineProps<{
statement: MathStatement,
size?: 'big' | 'small',
}>()
const getRenderedHTML = () => props.statement.toHTMLString()
const renderedHtml = getRenderedHTML()
computed(getRenderedHTML)
</script>
<style>
.big {
transform: scale(1.3);
}
.small {
transform: scale(0.9);
}
</style>
<template>
<div ref="container" class="big" v-if="props?.size === 'big'" v-html="renderedHtml"></div>
<div ref="container" class="small" v-else-if="props?.size === 'small'" v-html="renderedHtml"></div>
<div ref="container" v-else v-html="renderedHtml"></div>
</template>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import {EvaluationResult, Maybe} from '../types'
import {MathStatement} from '../support/parse'
import {computed} from 'vue'
import Katex from './Katex.vue'
const props = defineProps<{
statement: MathStatement,
evaluation: EvaluationResult,
}>()
const getValueStatement = (): Maybe<MathStatement> => {
const value = props.evaluation.statements[props.statement.id]
if ( value ) {
return MathStatement.temp(String(value))
}
}
let value = getValueStatement()
computed(() => value = getValueStatement())
</script>
<style>
.math-statement {
border: 1px solid #ccc;
border-radius: 3px;
padding: 10px;
display: flex;
flex-direction: row;
}
.content {
flex: 1;
}
.sidebar {
padding-left: 10px;
}
</style>
<template>
<div class="math-statement">
<div class="content">
<Katex :statement="statement" size="big"/>
<div class="result" v-if="value">
<hr v-if="value" style="border: 1px solid #ccc; border-bottom: 0">
<Katex :statement="value" size="small" style="color: #666"/>
</div>
</div>
<div class="sidebar">
<button>
<img src="../assets/edit.svg" alt="Edit" height="16">
</button>
</div>
</div>
</template>

View File

@@ -1,11 +1,55 @@
/*
--------------------------------------------------
Vue Helpers
--------------------------------------------------
*/
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
/*
--------------------------------------------------
App UI
--------------------------------------------------
*/
import { Quasar } from 'quasar'
// Import icon libraries
import '@quasar/extras/roboto-font-latin-ext/roboto-font-latin-ext.css'
import '@quasar/extras/material-icons/material-icons.css'
import '@quasar/extras/fontawesome-v6/fontawesome-v6.css'
// A few examples for animations from Animate.css:
// import @quasar/extras/animate/fadeIn.css
// import @quasar/extras/animate/fadeOut.css
// Import Quasar css
import 'quasar/src/css/index.sass'
/*
--------------------------------------------------
Plugins
--------------------------------------------------
*/
import { DraggablePlugin } from '@braks/revue-draggable'
import { createAuth0 } from '@auth0/auth0-vue'
import 'katex/dist/katex.min.css'
import 'katex/dist/contrib/auto-render.min'
/*
--------------------------------------------------
Components
--------------------------------------------------
*/
import App from './App.vue'
const app = createApp(App)
app.use(Quasar, {
plugins: {}, // import Quasar plugins and add here
})
app.use(
createAuth0({
domain: 'dev-ge84r-eu.us.auth0.com',
@@ -14,5 +58,7 @@ app.use(
}),
)
app.use(router)
app.use(DraggablePlugin)
app.mount('#app')

19
src/pages/Editor.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
</script>
<template>
<Draggable>
<div class="box">
<q-btn
@click="showNotification"
color="primary"
label="Show another notification"
/>
</div>
</Draggable>
</template>
<style></style>

21
src/pages/Scratch.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import {MathPage} from '../support/page'
import {v4 as uuidv4} from 'uuid'
import Statement from '../components/Statement.vue'
import {MathStatement} from '../support/parse'
const page = new MathPage(uuidv4())
const stmt1Id = page.addRaw('x = y+3/4')
const stmt2Id = page.addRaw('y = 9')
const evaluation = page.evaluate()
const stmt = page.getStatement(stmt1Id)
console.log({page, stmt1Id})
const onEdit = (stmt: MathStatement) => () => console.log('edit', stmt)
</script>
<template>
<p>Scratch page for testing!</p>
<Statement v-if="stmt" :statement="stmt" :evaluation="evaluation"/>
</template>

10
src/quasar-variables.sass Normal file
View File

@@ -0,0 +1,10 @@
$primary : #1976D2
$secondary : #26A69A
$accent : #9C27B0
$dark : #1D1D1D
$positive : #21BA45
$negative : #C10015
$info : #31CCEC
$warning : #F2C037

27
src/router.ts Normal file
View File

@@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from './pages/Login.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/scratch',
name: 'Scratch',
component: () => import(/* webpackChunkName: "scratch" */ './pages/Scratch.vue'),
},
{
path: '/editor',
name: 'Editor',
component: () => import('./pages/Editor.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
export default router

112
src/support/page.ts Normal file
View File

@@ -0,0 +1,112 @@
import {MathStatement} from './parse'
import * as math from 'mathjs'
import {DepGraph} from 'dependency-graph'
import { v4 as uuidv4 } from 'uuid'
import {EvaluationResult, Maybe, StatementID, VariableName} from '../types'
/**
* Wrapper for a page containing multiple interrelated mathematical statements.
*/
export class MathPage {
/** The statements on the page. */
protected statements: Record<StatementID, MathStatement> = {}
constructor(
/** Unique page ID. */
public readonly id: string,
) {}
/** Get a statement by ID if it exists. */
getStatement(id: StatementID): Maybe<MathStatement> {
return this.statements[id]
}
/** Add a statement to this page. */
addStatement(statement: MathStatement): this {
this.statements[statement.id] = statement
return this
}
/** Parse the math expression and add it to the page as a statement. */
addRaw(statement: string): StatementID {
const stmt = new MathStatement(uuidv4() as StatementID, statement)
this.addStatement(stmt)
return stmt.id
}
/** Get all symbols referenced by statements on this page. */
symbols(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.symbols())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get all symbols defined on this page. */
defines(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.defines())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get all symbols used on this page. */
uses(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.uses())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get a mapping of symbol names to the statements where they are defined. */
definers(): Record<VariableName, MathStatement> {
const definers: Record<VariableName, MathStatement> = {}
for ( const statement of Object.values(this.statements) ) {
for ( const symbol of statement.defines() ) {
definers[symbol.name as VariableName] = statement
}
}
return definers
}
/** Get the dependency graph of variable declarations between statements on this page. */
dependencies(): DepGraph<MathStatement> {
const graph = new DepGraph<MathStatement>()
const defined: Record<VariableName, MathStatement> = this.definers()
for ( const statement of Object.values(this.statements) ) {
graph.addNode(statement.id, statement)
}
for ( const statement of Object.values(this.statements) ) {
for ( const symbol of statement.uses() ) {
const provider = defined[symbol.name as VariableName]
if ( !provider ) {
throw new Error('No provider for undefined symbol: ' + symbol.name)
}
graph.addDependency(statement.id, provider.id)
}
}
return graph
}
/** Evaluate the current state of the page and get the result. */
evaluate(): EvaluationResult {
const evaluations: Record<StatementID, any> = {}
const scope: Record<VariableName, any> = {}
const graph = this.dependencies()
for ( const node of graph.overallOrder() ) {
const stmt = this.statements[node as StatementID]
evaluations[stmt.id] = stmt.parse()
.compile()
.evaluate(scope)
}
return {
variables: scope,
statements: evaluations,
}
}
}

317
src/support/parse.ts Normal file
View File

@@ -0,0 +1,317 @@
import * as math from 'mathjs'
import katex from 'katex'
import {HTMLString, LaTeXString, StatementID} from '../types'
import {v4 as uuidv4} from 'uuid'
/** Base class for walks over MathNode trees. */
export abstract class MathNodeWalk<TReturn> {
walk(node: math.MathNode): TReturn {
if ( math.isAccessorNode(node) ) {
return this.walkAccessorNode(node)
} else if ( math.isArrayNode(node) ) {
return this.walkArrayNode(node)
} else if ( math.isAssignmentNode(node) ) {
return this.walkAssignmentNode(node)
} else if ( math.isBlockNode(node) ) {
return this.walkBlockNode(node)
} else if ( math.isConditionalNode(node) ) {
return this.walkConditionalNode(node)
} else if ( math.isConstantNode(node) ) {
return this.walkConstantNode(node)
} else if ( math.isFunctionAssignmentNode(node) ) {
return this.walkFunctionAssignmentNode(node)
} else if ( math.isFunctionNode(node) ) {
return this.walkFunctionNode(node)
} else if ( math.isIndexNode(node) ) {
return this.walkIndexNode(node)
} else if ( math.isObjectNode(node) ) {
return this.walkObjectNode(node)
} else if ( math.isOperatorNode(node) ) {
return this.walkOperatorNode(node)
} else if ( math.isParenthesisNode(node) ) {
return this.walkParenthesisNode(node)
} else if ( math.isRangeNode(node) ) {
return this.walkRangeNode(node)
} else if ( (node as unknown as any).isRelationalNode ) {
return this.walkRelationalNode(node as unknown as any)
} else if ( math.isSymbolNode(node) ) {
return this.walkSymbolNode(node)
}
throw new TypeError('Invalid MathNode: ' + node)
}
abstract walkAccessorNode(node: math.AccessorNode): TReturn
abstract walkArrayNode(node: math.ArrayNode): TReturn
abstract walkAssignmentNode(node: math.AssignmentNode): TReturn
abstract walkBlockNode(node: math.BlockNode): TReturn
abstract walkConditionalNode(node: math.ConditionalNode): TReturn
abstract walkConstantNode(node: math.ConstantNode): TReturn
abstract walkFunctionAssignmentNode(node: math.FunctionAssignmentNode): TReturn
abstract walkFunctionNode(node: math.FunctionNode): TReturn
abstract walkIndexNode(node: math.IndexNode): TReturn
abstract walkObjectNode(node: math.ObjectNode): TReturn
abstract walkOperatorNode(node: math.OperatorNode): TReturn
abstract walkParenthesisNode(node: math.ParenthesisNode): TReturn
abstract walkRangeNode(node: math.RangeNode): TReturn
abstract walkRelationalNode(node: math.RelationalNode): TReturn
abstract walkSymbolNode(node: math.SymbolNode): TReturn
}
/** A walk that accumulates all different SymbolNode instances in a tree. */
export class SymbolWalk extends MathNodeWalk<math.SymbolNode[]> {
walkAccessorNode(node: math.AccessorNode): math.SymbolNode[] {
return [
...this.walk(node.object),
...this.walk(node.index),
]
}
walkArrayNode(node: math.ArrayNode): math.SymbolNode[] {
return node.items
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), [])
}
walkAssignmentNode(node: math.AssignmentNode): math.SymbolNode[] {
return [
...this.walk(node.object),
...this.walk(node.value),
...(node.index ? this.walk(node.index) : []),
]
}
walkBlockNode(node: math.BlockNode): math.SymbolNode[] {
return node.blocks
.map(x => x.node)
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), [])
}
walkConditionalNode(node: math.ConditionalNode): math.SymbolNode[] {
return [
...this.walk(node.condition),
...this.walk(node.trueExpr),
...this.walk(node.falseExpr),
]
}
walkConstantNode(): math.SymbolNode[] {
return []
}
walkFunctionAssignmentNode(node: math.FunctionAssignmentNode): math.SymbolNode[] {
return this.walk(node.expr)
}
walkFunctionNode(node: math.FunctionNode): math.SymbolNode[] {
return [
...this.walk(node.fn),
...node.args
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), []),
]
}
walkIndexNode(node: math.IndexNode): math.SymbolNode[] {
return node.dimensions
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), [])
}
walkObjectNode(node: math.ObjectNode): math.SymbolNode[] {
return Object.values(node.properties)
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), [])
}
walkOperatorNode(node: math.OperatorNode): math.SymbolNode[] {
return node.args
.map(x => this.walk(x))
.reduce((carry, current) => carry.concat(current), [])
}
walkParenthesisNode(node: math.ParenthesisNode): math.SymbolNode[] {
return this.walk(node.content)
}
walkRangeNode(node: math.RangeNode): math.SymbolNode[] {
return [
...this.walk(node.start),
...this.walk(node.end),
...(node.step ? this.walk(node.step) : []),
]
}
walkRelationalNode(node: math.RelationalNode): math.SymbolNode[] {
return node.params
.map(x => this.walk(x))
.reduce((carry, current) => carry.concat(current), [])
}
walkSymbolNode(node: math.SymbolNode): math.SymbolNode[] {
return [node]
}
}
/** A walk that accumulates all SymbolNode instances used on the RHS of expressions. */
export class RValSymbolWalk extends SymbolWalk {
walkAssignmentNode(node: math.AssignmentNode): math.SymbolNode[] {
return this.walk(node.value)
}
walkFunctionNode(node: math.FunctionNode): math.SymbolNode[] {
return [
...this.walk(node.fn), // FIXME should this be removed? Not sure if this is rval or lval
...node.args
.map(x => this.walk(x))
.reduce((carry, current) => current.concat(carry), []),
]
}
}
/** A walk that accumulates SymbolNode instances used on the LHS of assignments. */
export class LValSymbolWalk extends SymbolWalk {
walkAccessorNode(): math.SymbolNode[] {
return []
}
walkArrayNode(): math.SymbolNode[] {
return []
}
walkAssignmentNode(node: math.AssignmentNode): math.SymbolNode[] {
if ( math.isSymbolNode(node.object) ) {
return super.walkSymbolNode(node.object)
}
return super.walkAccessorNode(node.object)
}
walkBlockNode(node: math.BlockNode): math.SymbolNode[] {
return node.blocks
.map(x => this.walk(x.node))
.reduce((carry, current) => current.concat(carry), [])
}
walkConditionalNode(): math.SymbolNode[] {
return []
}
walkConstantNode(): math.SymbolNode[] {
return []
}
walkFunctionAssignmentNode(): math.SymbolNode[] {
return []
}
walkFunctionNode(): math.SymbolNode[] {
return []
}
walkIndexNode(): math.SymbolNode[] {
return []
}
walkObjectNode(): math.SymbolNode[] {
return []
}
walkOperatorNode(): math.SymbolNode[] {
return []
}
walkParenthesisNode(node: math.ParenthesisNode): math.SymbolNode[] {
return this.walk(node.content)
}
walkRangeNode(): math.SymbolNode[] {
return []
}
walkRelationalNode(): math.SymbolNode[] {
return []
}
walkSymbolNode(): math.SymbolNode[] {
return []
}
}
/** A single mathematical statement. */
export class MathStatement {
static temp(raw: string): MathStatement {
return new MathStatement(uuidv4() as StatementID, raw)
}
constructor(
/** Unique ID of this statement. */
public readonly id: StatementID,
/** The raw statement input by the user. */
public readonly raw: string,
) {}
/** Parse the raw statement to an AST. */
parse(): math.MathNode {
return math.parse(this.raw)
}
/** Convert the statement to its equivalent LaTeX code. */
toLaTeX(): LaTeXString {
return this.parse().toTex() as LaTeXString
}
/** Render the statement as HTML string. */
toHTMLString(): HTMLString {
return katex.renderToString(this.toLaTeX(), {
output: 'html',
}) as HTMLString
}
/** Render the statement to a DOM element. */
toDOM(node?: HTMLElement): HTMLSpanElement {
if ( !node ) {
node = document.createElement('span')
}
katex.render(this.toLaTeX(), node, {
output: 'html',
})
return node
}
/** Get all symbols referenced in this statement. */
symbols(): math.SymbolNode[] {
return (new SymbolWalk()).walk(this.parse())
}
/** Get all symbols defined on the LHS of this statement. */
defines(): math.SymbolNode[] {
return (new LValSymbolWalk()).walk(this.parse())
}
/** Get all symbols used on the RHS of this statement. */
uses(): math.SymbolNode[] {
return (new RValSymbolWalk()).walk(this.parse())
}
}

View File

@@ -80,3 +80,14 @@ export type Integer = TypeTag<'@app.Integer'> & number
export function isInteger(num: number): num is Integer {
return !isNaN(num) && parseInt(String(num), 10) === num
}
export type LaTeXString = TypeTag<'@app.LaTeXString'> & string
export type HTMLString = TypeTag<'@app.HTMLString'> & string
export type StatementID = TypeTag<'@app.StatementID'> & string
export type VariableName = TypeTag<'@app.VariableName'> & string
export type EvaluatedValue = TypeTag<'@app.EvaluatedValue'> & string
export interface EvaluationResult {
variables: Record<VariableName, any>
statements: Record<StatementID, any>
}