Frontend and communication

This commit is contained in:
jorenchik
2024-09-01 10:53:46 +03:00
parent e824115fae
commit ebdb5bed62
11 changed files with 463 additions and 227 deletions

View File

@@ -2,44 +2,44 @@ package parser
import (
"fmt"
"strings"
"log"
"github.com/jorenchik/mdemory/src/compiler/lexer"
"log"
"strings"
)
type Question interface {
ToString() string;
ToString() string
}
type SingleAnswerQuestion struct {
id string;
question string;
answer string;
section string;
ID string
Question string
Answer string
Section string
}
type Choice struct {
answer string;
isCorrect bool;
answer string
isCorrect bool
}
type MultipleChoiceQuestion struct {
id string;
question string;
choices []Choice;
section string;
id string
question string
choices []Choice
section string
}
func (question SingleAnswerQuestion)ToString() string {
func (question SingleAnswerQuestion) ToString() string {
return fmt.Sprintf(
"<Single choice> (%s) %s: %s\n",
question.section,
strings.Trim(question.question, "\t\n "),
strings.Trim(question.answer, "\t\n "),
question.Section,
strings.Trim(question.Question, "\t\n "),
strings.Trim(question.Answer, "\t\n "),
)
}
func (question MultipleChoiceQuestion)ToString() string {
func (question MultipleChoiceQuestion) ToString() string {
acc := ""
acc += fmt.Sprintf(
"<Multi choice> (%s) %s\n",
@@ -48,7 +48,7 @@ func (question MultipleChoiceQuestion)ToString() string {
)
for _, el := range question.choices {
opener := '-'
if (el.isCorrect) {
if el.isCorrect {
opener = '+'
}
acc += fmt.Sprintf("\t%c %s\n", opener, strings.Trim(el.answer, "\t\n "))
@@ -57,16 +57,16 @@ func (question MultipleChoiceQuestion)ToString() string {
}
type QuestionElement struct {
isDash bool;
content string;
}
isDash bool
content string
}
var automata map[lexer.TokenType][]lexer.TokenType
var automata map[lexer.TokenType][]lexer.TokenType
type CompilerErr struct {
message string;
row int32;
column int32;
message string
row int32
column int32
}
func (e CompilerErr) Error() string {
@@ -74,12 +74,12 @@ func (e CompilerErr) Error() string {
}
func contains(s []lexer.TokenType, e lexer.TokenType) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
for _, a := range s {
if a == e {
return true
}
}
return false
}
func parserAutomata() map[lexer.TokenType][]lexer.TokenType {
@@ -95,22 +95,22 @@ func parserAutomata() map[lexer.TokenType][]lexer.TokenType {
}
automata[lexer.QuestionEnd] = []lexer.TokenType{
lexer.ElementDashStart, lexer.ElementPlusStart,
}
}
automata[lexer.ElementDashStart] = []lexer.TokenType{
lexer.IdentifierStart, lexer.TextFragment,
}
}
automata[lexer.ElementPlusStart] = []lexer.TokenType{
lexer.TextFragment,
}
}
automata[lexer.Identifier] = []lexer.TokenType{
lexer.IdentifierEnd, lexer.SectionStart,
}
}
automata[lexer.IdentifierStart] = []lexer.TokenType{
lexer.Identifier,
}
}
automata[lexer.IdentifierEnd] = []lexer.TokenType{
lexer.TextFragment,
}
}
automata[lexer.SectionIdentifierStart] = []lexer.TokenType{
lexer.Identifier,
}
@@ -129,30 +129,30 @@ func parserAutomata() map[lexer.TokenType][]lexer.TokenType {
func ValidateGrammar(tokens []lexer.Token) error {
automata = parserAutomata()
for i := 0; i < len(tokens) - 1; i++ {
for i := 0; i < len(tokens)-1; i++ {
token := tokens[i]
nextToken := tokens[i + 1]
if (!contains(automata[token.TokenType], nextToken.TokenType)) {
nextToken := tokens[i+1]
if !contains(automata[token.TokenType], nextToken.TokenType) {
return CompilerErr{
message: fmt.Sprintf(
"Token %s cannot precede %s\n",
lexer.ToString(&token.TokenType),
lexer.ToString(&nextToken.TokenType),
),
row: token.Row,
row: token.Row,
column: token.Column,
}
}
}
}
return nil
}
func ParseQuestions(fileContents string) ([]Question, error) {
tokens, err := lexer.TokenizeMdem([]rune(fileContents))
if (err != nil) {
if err != nil {
return nil, err
}
if (true) {
if true {
log.Println("Lexer output:")
for _, el := range tokens {
fmt.Print(el.ToString())
@@ -160,7 +160,7 @@ func ParseQuestions(fileContents string) ([]Question, error) {
}
err = ValidateGrammar(tokens)
if (err != nil) {
if err != nil {
log.Fatal(err.Error())
}
@@ -168,26 +168,26 @@ func ParseQuestions(fileContents string) ([]Question, error) {
section := ""
i := 0
for {
if (i >= len(tokens)) {
if i >= len(tokens) {
break
}
if (tokens[i].TokenType == lexer.ElementDashStart) {
id := tokens[i + 2].Content
question := tokens[i + 4].Content
if tokens[i].TokenType == lexer.ElementDashStart {
id := tokens[i+2].Content
question := tokens[i+4].Content
quesitonElements := []QuestionElement{}
i += 6
for {
if (i + 1 >= len(tokens) ||
if i+1 >= len(tokens) ||
!(tokens[i].TokenType == lexer.ElementDashStart ||
tokens[i].TokenType == lexer.ElementPlusStart) ||
tokens[i+1].TokenType == lexer.IdentifierStart) {
tokens[i+1].TokenType == lexer.IdentifierStart {
break
}
questionElement := QuestionElement{}
if (tokens[i].TokenType == lexer.ElementDashStart) {
questionElement.isDash = true
if tokens[i].TokenType == lexer.ElementDashStart {
questionElement.isDash = true
} else {
questionElement.isDash = false
questionElement.isDash = false
}
questionElement.content = tokens[i+1].Content
quesitonElements = append(quesitonElements, questionElement)
@@ -195,7 +195,7 @@ func ParseQuestions(fileContents string) ([]Question, error) {
}
if len(quesitonElements) > 1 {
question := MultipleChoiceQuestion{
id: id,
id: id,
question: question,
}
choices := []Choice{}
@@ -204,30 +204,30 @@ func ParseQuestions(fileContents string) ([]Question, error) {
choice.answer = quesitonElements[k].content
choice.isCorrect = !quesitonElements[k].isDash
choices = append(choices, choice)
}
if (section != "") {
}
if section != "" {
question.section = section
}
question.choices = choices
questions = append(questions, question)
} else if (len(quesitonElements) == 1) {
} else if len(quesitonElements) == 1 {
question := SingleAnswerQuestion{
id: id,
question: question,
answer: quesitonElements[0].content,
ID: id,
Question: question,
Answer: quesitonElements[0].content,
}
if (section != "") {
question.section = section
if section != "" {
question.Section = section
}
questions = append(questions, question)
}
} else if (tokens[i].TokenType == lexer.SectionIdentifierStart) {
section = tokens[i + 1].Content
i += 3;
} else if (tokens[i].TokenType == lexer.SectionEnd) {
} else if tokens[i].TokenType == lexer.SectionIdentifierStart {
section = tokens[i+1].Content
i += 3
} else if tokens[i].TokenType == lexer.SectionEnd {
section = ""
i += 1
} else if (tokens[i].TokenType == lexer.EOF) {
} else if tokens[i].TokenType == lexer.EOF {
break
} else {
log.Fatalf(
@@ -236,8 +236,8 @@ func ParseQuestions(fileContents string) ([]Question, error) {
)
return nil, CompilerErr{
message: "",
row: tokens[i].Row,
column: tokens[i].Column,
row: tokens[i].Row,
column: tokens[i].Column,
}
}
}

16
src/mdemory-app/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Wails: build debug",
"type": "go",
"request": "launch",
"mode": "exec",
"program": "${workspaceFolder}/myapp",
"preLaunchTask": "wails_debug_build",
"env": {},
"args": []
}
]
}

11
src/mdemory-app/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "wails_debug_build",
"type": "shell",
"command": "wails build -debug"
}
]
}

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/jorenchik/mdemory/src/compiler/parser"
"os"
"regexp"
)
@@ -28,30 +29,76 @@ func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
type File struct{
Name string;
IsDir bool;
type File struct {
Name string
IsDir bool
}
// Greet returns a greeting for the given name
func (a *App) GetRepoFiles() []File {
dirPath := "/home/jorenchik/Code/mdemory/memorybase/"
func (a *App) GetRepoFiles(dirPath string) []File {
dirEntries, err := os.ReadDir(dirPath)
if (err != nil) {
if err != nil {
return []File{}
}
files := []File{}
exp, err := regexp.Compile(".+\\.mdem")
for _, el := range dirEntries {
if (err != nil) {
if err != nil {
continue
}
if (el.IsDir() || exp.Match([]byte(el.Name()))) {
if el.IsDir() || exp.Match([]byte(el.Name())) {
file := File{}
file.Name = el.Name()
file.IsDir = el.IsDir()
files = append(files, file)
}
}
return files
return files
}
func (a *App) ParseFile(filePath string) (string, error) {
fmt.Printf("Parse file requested. Path: %s\n", filePath)
dirName := "/home/jorenchik/Code/mdemory/memorybase/"
dir, err := os.ReadDir(dirName)
for _, el := range dir {
if err != nil {
continue
}
path := dirName + el.Name()
fmt.Println(path)
bytes, err := os.ReadFile(path)
if err != nil {
continue
}
fmt.Println(string(bytes))
}
bytes, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("File opening error: %s\n", err.Error())
return "", err
}
fileContents := string(bytes)
questions, err := parser.ParseQuestions(fileContents)
if err != nil {
fmt.Printf("Parsing error: %s\n", err.Error())
return "", err
}
for i := 0; i < len(questions); i++ {
question := questions[i]
switch q := question.(type) {
case parser.SingleAnswerQuestion:
fmt.Printf(
"Single Answer: question: %s; answer: %s",
q.Question,
q.Answer,
)
}
}
fmt.Printf("Returning file. Path: %s\n", filePath)
return fileContents, nil
}

View File

@@ -1,75 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>mdemory-app</title>
</head>
<body>
<div id="app">
<div class="main-layout">
<div>
<h3>memorybase</h3>
<div class="file-menus">
<h4 class="sidebar-heading">mdems</h4>
<div>
<div id="deck-list" class="deck-list">
<div class="mbase-actions">
<input class="repo-input" type="text" name="file" value="">
<a id="reload-files" href="#">
Reload
</a>
</div>
<!--<a class="deck-link" href="#">-->
<!-- <div class="deck">-->
<!-- <span>&gt;</span>-->
<!-- <span>math</span>-->
<!-- </div>-->
<!--</a>-->
<!--<a class="deck-link" href="#">-->
<!-- <div class="deck">-->
<!-- cpp-->
<!-- </div>-->
<!--</a>-->
</div>
</div>
<div>
<h4 class="sidebar-heading">decks</h4>
<div class="deck-list">
<a class="deck-link" href="#">
<div class="deck">
cpp
</div>
</a>
<a class="deck-link" href="#">
<div class="deck">
python
</div>
</a>
<a class="deck-link" href="#">
<div class="deck">
tikli
</div>
</a>
</div>
</div>
</div>
</div>
<div>
<h2>flashcards: countries</h2>
<div class="deck-listing">
<div class="flashcard">
<span>&gt;</span>
<span>What is the capital of Latvia?</span>
</div>
<div class="flashcard">
<span>&gt;</span>
<span>What is the capital of Lithuania?</span>
</div>
</div>
</div>
</div>
</div>
<script src="./src/main.ts" type="module"></script>
<div class="master" id="app">
<div class="top-menu">
<div class="option">
<span>Base</span>
<div class="list">
<div>Save</div>
<div>Open</div>
<div>Reload</div>
</div>
</div>
<div class="">
Help
</div>
</div>
<div class="main-layout">
<div class="file-menus">
<div class="mbase-actions">
<h3>memorybase</h3>
<div class="mbase-search">
<input class="repo-input" type="text" name="file" value="">
<a id="reload-files" href="#">
Reload
</a>
</div>
</div>
<div class="mbase-mdems">
<h4 class="sidebar-heading">mdems</h4>
<div id="deck-list" class="deck-list">
<!-- Mdem example -->
<!--<a class="deck-link" href="#">-->
<!-- <div class="deck">-->
<!-- <span>&gt;</span>-->
<!-- <span>math</span>-->
<!-- </div>-->
<!--</a>-->
<!--<a class="deck-link" href="#">-->
<!-- <div class="deck">-->
<!-- cpp-->
<!-- </div>-->
<!--</a>-->
</div>
</div>
<div class="mbase-decks">
<!-- Deck example -->
<h4 class="sidebar-heading">decks</h4>
<div>
<a class="deck-link" href="#">
<div class="deck">
cpp
</div>
</a>
<a class="deck-link" href="#">
<div class="deck">
python
</div>
</a>
<a class="deck-link" href="#">
<div class="deck">
tikli
</div>
</a>
</div>
</div>
</div>
<div>
<h2>countries.mdem</h2>
<div class="deck-listing-box">
<div class="deck-listing-top">
<div>
<span>Flashcards: 12.</span>
<span>Page: 1 of 3.</span>
</div>
<div>
<button type="button">Train</button>
<button type="button">View stats</button>
</div>
</div>
<div class="deck-listing">
<div>
<div class="flashcard">
<span>&gt;</span>
<span>What is the capital of Latvia?</span>
</div>
</div>
<div>
<div class="flashcard">
<span>&gt;</span>
<span>What is the capital of Lithuania?</span>
</div>
</div>
<div>
<div class="flashcard">
<span>&gt;</span>
<span>What is the capital of Estonia?</span>
</div>
<div class="answer">
<span>-</span>
<span>Tallin</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

View File

@@ -1,26 +1,60 @@
.main-layout {
display: grid;
grid-template-columns: .35fr 1fr; /* Creates 3 equal columns */
height: 100%;
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;
color: white;
}
.main-layout > div:nth-child(1) {
/*background: white;*/
border-right: 1px solid gray;
}
.main-layout .deck-list {
padding: .5rem 1rem;
display: flex;
flex-direction: column;
/*gap: .5rem;*/
body {
box-sizing: content-box;
margin: 0;
color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
a {
color: unset;
text-decoration: unset;
color: unset;
text-decoration: unset;
}
h1, h2, h3, h4 {
font-size: 1.1rem;
}
#app {
text-align: center;
height: 100vh;
display: flex;
flex-direction: column;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
.main-layout {
display: grid;
grid-template-columns: .35fr 1fr;
flex-grow: 1;
/* height: 100%; */
}
.main-layout > div:nth-child(1) {
border-right: 1px solid gray;
}
.main-layout .file-menus {
padding: 0 20px;
display: flex;
flex-direction: column;
}
.main-layout .deck {
border: 1px solid gray;
border-top: none;
@@ -36,30 +70,55 @@ a {
border-top: 1px solid gray;
}
h1, h2, h3, h4 {
font-size: 1.1rem;
}
.deck-listing {
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
}
.deck-listing-top {
display: flex;
justify-content: space-between;
width: 100%;
margin: 20px 0;
}
.deck-listing-box {
padding: 0 20px;
}
.deck-listing .flashcard {
display: flex;
align-items: center;
border: 1px solid white;
height: 80px;
border-radius: .3rem;
border: 1px solid white;
border-top-left-radius: .3rem;
border-top-right-radius: .3rem;
border-bottom-left-radius: .3rem;
border-bottom-right-radius: .3rem;
}
.deck-listing .flashcard > * {
margin-left: 15px;
}
.deck-listing .answer {
margin-top: -2px;
display: flex;
align-items: center;
height: 80px;
border: 1px solid white;
border-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: .3rem;
border-bottom-right-radius: .3rem;
}
.deck-listing .answer > * {
padding-left: 15px;
}
.repo-input {
width: 99%;
height: 25px;
@@ -68,11 +127,19 @@ h1, h2, h3, h4 {
.mbase-actions {
display: flex;
gap: 0rem;
flex-direction: column;
align-items: center;
gap: .5rem;
margin-bottom: 1rem;
}
.mbase-search {
display: flex;
gap: .5rem;
align-items: center;
width: 100%;
}
.mbase-actions a {
font-size: .8rem;
display: block;
@@ -82,11 +149,72 @@ h1, h2, h3, h4 {
font-size: .9rem;
display: flex;
margin: 0;
padding: 0 1rem;
padding: 10px 0;
}
.file-menus {
height: 100%;
}
.file-menus > div {
margin-bottom: 1.5rem;
}
.mbase-actions {
height: 10%;
}
.mbase-mdems {
max-height: 40%;
}
.mbase-decks {
max-height: 40%;
}
.top-menu {
position: sticky;
display: flex;
width: 100%;
background: gray;
border: 1px solid white;
}
.top-menu > div {
cursor: pointer;
border-right: 1px solid white;
padding: 1px 10px;
}
.top-menu .option {
position: relative;
}
.top-menu .option .list {
position: absolute;
top: 100%;
left: 0;
background: gray;
width: 150px;
/* display: flex; */
display: none;
flex-direction: column;
text-align: left;
padding: 0;
margin: 0;
border: 1px solid white;
}
.top-menu .option .list > div {
padding: 0 0 0 15px;
margin: 0;
}
.top-menu .option .list > div:nth-child(1) {
border-top: 0;
}
.top-menu .option .list > div {
border-top: 1px solid white;
}

View File

@@ -1,7 +1,7 @@
import './style.css';
import './app.css';
import {GetRepoFiles} from '../wailsjs/go/main/App';
import {GetRepoFiles, ParseFile} from '../wailsjs/go/main/App';
import {main} from '../wailsjs/go/models';
interface DeckFile {
@@ -9,49 +9,56 @@ interface DeckFile {
IsDir: boolean;
}
// console.log(await ParseFile("fjoewjf"));
function fetchFiles() {
GetRepoFiles().then((result: main.File[]) => {
GetRepoFiles("/home/jorenchik/Code/mdemory/memorybase/")
.then((result: main.File[]) => {
const files: DeckFile[] = result as DeckFile[]
setDecklistFiles(files)
});
}
fetchFiles()
let mbasePath = "/home/jorenchik/Code/mdemory/memorybase/"
async function fetchParsed(filename: string) {
console.log(filename);
try {
console.log(await ParseFile(mbasePath + filename));
} catch (error) {
console.error("Error:", error);
}
// let time = new Date().getTime();
// console.log(await ParseFile(mbasePath + filename));
// console.log(new Date().getTime() - time)
}
let decklist: Element | null = document.querySelector("#deck-list");
function setDecklistFiles(files: DeckFile[]) {
if (decklist) {
// removeAnchors
let deckFileAnchors = decklist.querySelectorAll(".deck-link")
deckFileAnchors.forEach(i => {
i.remove();
});
// createAnchors
for (let i = 0; i < files.length; i++) {
let element = document.createElement('a');
let angleHtml = ''
if (files[i].IsDir) {
angleHtml = "<span angle='true'>></span>"
}
element.setAttribute("href", "#")
element.setAttribute("class", "deck-link")
element.innerHTML = `
<div class="deck">
${ angleHtml }
${files[i].Name}
</div>
`
element.setAttribute("href", "#")
element.setAttribute("class", "deck-link")
decklist.appendChild(element)
if (files[i].IsDir) {
let angle: HTMLElement | null = element.querySelector("[angle='true']")
element.addEventListener("click", _clickable => {
let style: CSSStyleDeclaration | undefined = angle?.style
if (style) {
if (style["rotate"] == "90deg") {
style["rotate"] = "";
} else {
style["rotate"] = "90deg";
}
}
})
}
element.addEventListener('click', _ => {
fetchParsed(element.innerText);
});
}
}
}

View File

@@ -1,26 +0,0 @@
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;
color: white;
}
body {
margin: 0;
color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
#app {
height: 100vh;
text-align: center;
}

View File

@@ -2,6 +2,8 @@
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function GetRepoFiles():Promise<Array<main.File>>;
export function GetRepoFiles(arg1:string):Promise<Array<main.File>>;
export function Greet(arg1:string):Promise<string>;
export function ParseFile(arg1:string):Promise<string>;

View File

@@ -2,10 +2,14 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetRepoFiles() {
return window['go']['main']['App']['GetRepoFiles']();
export function GetRepoFiles(arg1) {
return window['go']['main']['App']['GetRepoFiles'](arg1);
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function ParseFile(arg1) {
return window['go']['main']['App']['ParseFile'](arg1);
}

View File

@@ -2,7 +2,10 @@ module mdemory-app
go 1.22.5
require github.com/wailsapp/wails/v2 v2.9.1
require (
github.com/jorenchik/mdemory/src/compiler v0.0.0-00010101000000-000000000000
github.com/wailsapp/wails/v2 v2.9.1
)
require (
github.com/bep/debounce v1.2.1 // indirect