0.0.3: Exotic Type Gymnastics #3
|  | @ -1,60 +0,0 @@ | ||||||
| name: Publish to npm |  | ||||||
| 
 |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - dev |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   quality: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout repository |  | ||||||
|         uses: actions/checkout@v2 |  | ||||||
|       - name: Set up Node.js |  | ||||||
|         uses: actions/setup-node@v2 |  | ||||||
|         with: |  | ||||||
|           node-version: '22' |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: npm ci |  | ||||||
|       - name: Run Biome |  | ||||||
|         run: npm run quality-check |  | ||||||
| 
 |  | ||||||
|   publish: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     needs: quality |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout repository |  | ||||||
|         uses: actions/checkout@v2 |  | ||||||
| 
 |  | ||||||
|       - name: Set up Node.js |  | ||||||
|         uses: actions/setup-node@v2 |  | ||||||
|         with: |  | ||||||
|           node-version: '22' |  | ||||||
|           registry-url: 'https://registry.npmjs.org/' |  | ||||||
| 
 |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: npm ci |  | ||||||
| 
 |  | ||||||
|       - name: Determine package name |  | ||||||
|         id: package_name |  | ||||||
|         run: | |  | ||||||
|           if [ "${{ gitea.ref }}" == "refs/heads/main" ]; then |  | ||||||
|             echo "PACKAGE_NAME=laterano" >> $GITEA_ENV |  | ||||||
|             echo "ACCESS_LEVEL=public" >> $GITEA_ENV |  | ||||||
|           elif [ "${{ gitea.ref }}" == "refs/heads/dev" ]; then |  | ||||||
|             echo "PACKAGE_NAME=@astrian/laterano-dev" >> $GITEA_ENV |  | ||||||
|             echo "ACCESS_LEVEL=restricted" >> $GITEA_ENV |  | ||||||
|           fi |  | ||||||
| 
 |  | ||||||
|       - name: Update package.json for dev releases |  | ||||||
|         if: GITEA.ref == 'refs/heads/dev' |  | ||||||
|         run: | |  | ||||||
|           jq --arg name "@astrian/laterano-dev" '.name=$name' package.json > temp.json && mv temp.json package.json |  | ||||||
|           jq --arg version "0.0.0-dev.$(date +%s)" '.version=$version' package.json > temp.json && mv temp.json package.json |  | ||||||
| 
 |  | ||||||
|       - name: Publish to npm |  | ||||||
|         run: npm publish --access $ACCESS_LEVEL |  | ||||||
|         env: |  | ||||||
|           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |  | ||||||
							
								
								
									
										129
									
								
								.gitea/workflows/workflow.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								.gitea/workflows/workflow.yaml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | ||||||
|  | name: Quality Check & Publish | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - '*' | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   quality: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |       - name: Set up Node.js | ||||||
|  |         uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '22' | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm ci | ||||||
|  |       - name: Run Biome | ||||||
|  |         run: npm run quality-check | ||||||
|  |     if: always() | ||||||
|  |     outputs: | ||||||
|  |       status: ${{ job.status }} | ||||||
|  |      | ||||||
|  |   quality-failed-webhook: | ||||||
|  |     needs: quality | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     if: ${{ needs.quality.outputs.status == 'failure' }} | ||||||
|  |     steps: | ||||||
|  |       - name: Send webhook | ||||||
|  |         run: | | ||||||
|  |           QUALITY_CHECK_GROUP="Laterano CI/CD" | ||||||
|  |           QUALITY_CHECK_TITLE="Quality Check Failed" | ||||||
|  |           QUALITY_CHECK_MESSAGE="Quality check failed for commit ${{ gitea.sha }} in ${{ gitea.repository }}" | ||||||
|  |            | ||||||
|  |           # URL-encode the message | ||||||
|  |           ENCODED_GROUP=$(echo "$MSG_GROUP" sed 's/%/%25/g; s/ /%20/g; s/\//%2F/g; s/\?/%3F/g; s/&/%26/g') | ||||||
|  |           ENCODED_TITLE=$(echo "$QUALITY_CHECK_TITLE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           ENCODED_MESSAGE=$(echo "$QUALITY_CHECK_MESSAGE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |            | ||||||
|  |           echo "Webhook URL:" | ||||||
|  |           echo "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}" | ||||||
|  | 
 | ||||||
|  |           curl -X GET "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}"           | ||||||
|  | 
 | ||||||
|  |   publish: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: quality | ||||||
|  |     if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  | 
 | ||||||
|  |       - name: Set up Node.js | ||||||
|  |         uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '22' | ||||||
|  |           registry-url: 'https://registry.npmjs.org/' | ||||||
|  | 
 | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm ci | ||||||
|  | 
 | ||||||
|  |       - name: Determine package name | ||||||
|  |         id: package_name | ||||||
|  |         run: | | ||||||
|  |           if [ "${{ gitea.ref }}" == "refs/heads/main" ]; then | ||||||
|  |             echo "PACKAGE_NAME=laterano" >> $GITEA_ENV | ||||||
|  |             echo "ACCESS_LEVEL=public" >> $GITEA_ENV | ||||||
|  |           elif [ "${{ gitea.ref }}" == "refs/heads/dev" ]; then | ||||||
|  |             echo "PACKAGE_NAME=@astrian/laterano-dev" >> $GITEA_ENV | ||||||
|  |             echo "ACCESS_LEVEL=restricted" >> $GITEA_ENV | ||||||
|  |           fi           | ||||||
|  | 
 | ||||||
|  |       - name: Update package.json for dev releases | ||||||
|  |         if: GITEA.ref == 'refs/heads/dev' | ||||||
|  |         run: | | ||||||
|  |           jq --arg name "@astrian/laterano-dev" '.name=$name' package.json > temp.json && mv temp.json package.json | ||||||
|  |           jq --arg version "0.0.0-dev.$(date +%s)" '.version=$version' package.json > temp.json && mv temp.json package.json | ||||||
|  |           echo VERSION_CODE=$(jq -r '.version' package.json) >> $GITEA_ENV | ||||||
|  |           echo PACKAGE_NAME="@astrian/laterano-dev" >> $GITEA_ENV           | ||||||
|  | 
 | ||||||
|  |       - name: Get version code | ||||||
|  |         if: GITEA.ref == 'refs/heads/main' | ||||||
|  |         run: | | ||||||
|  |           echo VERSION_CODE=$(jq -r '.version' package.json) >> $GITEA_ENV | ||||||
|  |           echo PACKAGE_NAME="laterano" >> $GITEA_ENV           | ||||||
|  | 
 | ||||||
|  |       - name: Publish to npm | ||||||
|  |         run: npm publish --access $ACCESS_LEVEL | ||||||
|  |         env: | ||||||
|  |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||||
|  |    | ||||||
|  |       - name: Send webhook | ||||||
|  |         run: | | ||||||
|  |           PACKAGE_NAME="${{ steps.package_name.outputs.PACKAGE_NAME }}" | ||||||
|  |           MSG_GROUP="Laterano CI/CD" | ||||||
|  |           MSG_TITLE="Package Published" | ||||||
|  |           MSG_MESSAGE="Branch ${{ gitea.ref }}: published to npm with version $VERSION_CODE" | ||||||
|  |            | ||||||
|  |           # URL-encode the message | ||||||
|  |           ENCODED_MESSAGE=$(echo "$MSG_MESSAGE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           ENCODED_GROUP=$(echo "$MSG_GROUP" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           ENCODED_TITLE=$(echo "$MSG_TITLE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |            | ||||||
|  |           echo "Webhook URL:" | ||||||
|  |           echo "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}" | ||||||
|  | 
 | ||||||
|  |           curl -X GET "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}"           | ||||||
|  | 
 | ||||||
|  |   publish-failed-webhook: | ||||||
|  |     needs: publish | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     if: ${{ needs.publish.outputs.status == 'failure' }} | ||||||
|  |     steps: | ||||||
|  |       - name: Send webhook | ||||||
|  |         run: | | ||||||
|  |           PACKAGE_NAME="${{ steps.package_name.outputs.PACKAGE_NAME }}" | ||||||
|  |           MSG_GROUP="Laterano CI/CD" | ||||||
|  |           MSG_TITLE="Package Publish Failed" | ||||||
|  |           MSG_MESSAGE="Package $PACKAGE_NAME failed to publish to npm with version $VERSION_CODE" | ||||||
|  |            | ||||||
|  |           # URL-encode the message | ||||||
|  |           ENCODED_MESSAGE=$(echo "$MSG_MESSAGE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           ENCODED_GROUP=$(echo "$MSG_GROUP" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           ENCODED_TITLE=$(echo "$MSG_TITLE" | sed 's/%/%25/g' | sed 's/ /%20/g' | sed 's/!/%21/g' | sed 's/"/%22/g' | sed 's/#/%23/g' | sed 's/\$/%24/g' | sed 's/&/%26/g' | sed 's/'"'"'/%27/g' | sed 's/(/%28/g' | sed 's/)/%29/g' | sed 's/\*/%2A/g' | sed 's/+/%2B/g' | sed 's/,/%2C/g' | sed 's/\//%2F/g' | sed 's/:/%3A/g' | sed 's/;/%3B/g' | sed 's/=/%3D/g' | sed 's/?/%3F/g' | sed 's/@/%40/g') | ||||||
|  |           echo "Webhook URL:" | ||||||
|  |           echo "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}" | ||||||
|  |           curl -X GET "https://bark.nas.astrian.moe/${{ secrets.BARK_TOKEN }}/${ENCODED_TITLE}/${ENCODED_MESSAGE}?group=${ENCODED_GROUP}"           | ||||||
							
								
								
									
										24
									
								
								.github/workflows/workflow.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/workflow.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | name: Quality Check | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - '*' | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   quality: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |       - name: Set up Node.js | ||||||
|  |         uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '22' | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm ci | ||||||
|  |       - name: Run Biome | ||||||
|  |         run: npm run quality-check | ||||||
|  |     if: always() | ||||||
|  |     outputs: | ||||||
|  |       status: ${{ job.status }} | ||||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -110,4 +110,8 @@ tmp/ | ||||||
| temp/ | temp/ | ||||||
| 
 | 
 | ||||||
| # macOS Finder metadata files | # macOS Finder metadata files | ||||||
| .DS_Store | .DS_Store | ||||||
|  | 
 | ||||||
|  | # vim file | ||||||
|  | *.swp | ||||||
|  | 
 | ||||||
|  |  | ||||||
							
								
								
									
										8638
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8638
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							|  | @ -1,14 +1,16 @@ | ||||||
| { | { | ||||||
| 	"name": "laterano", | 	"name": "laterano", | ||||||
| 	"version": "0.0.2", | 	"version": "0.0.3", | ||||||
| 	"main": "dist/main.min.js", | 	"main": "dist/main.min.js", | ||||||
| 	"types": "dist/types.d.ts", | 	"types": "dist/types/main.d.ts", | ||||||
| 	"module": "dist/main.min.js", | 	"module": "dist/main.min.js", | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"build": "tsc && rollup -c && npm run cleanup-intermediate", | 		"build": "tsc && rollup -c && npm run cleanup-intermediate", | ||||||
| 		"prepare": "npm run build", | 		"prepare": "npm run build", | ||||||
| 		"cleanup-intermediate": "rimraf dist/main.js dist/types", | 		"cleanup-intermediate": "rimraf dist/main.js dist/utils", | ||||||
| 		"quality-check": "biome ci ." | 		"quality-check": "biome ci .", | ||||||
|  | 		"qc": "npm run quality-check", | ||||||
|  | 		"lint": "biome format . --write" | ||||||
| 	}, | 	}, | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import dts from 'rollup-plugin-dts' | ||||||
| 
 | 
 | ||||||
| export default [ | export default [ | ||||||
| 	{ | 	{ | ||||||
| 		input: 'dist/main.js', | 		input: 'src/main.ts', | ||||||
| 		output: [ | 		output: [ | ||||||
| 			{ | 			{ | ||||||
| 				file: 'dist/main.min.js', | 				file: 'dist/main.min.js', | ||||||
|  | @ -15,10 +15,20 @@ export default [ | ||||||
| 		], | 		], | ||||||
| 		plugins: [resolve(), typescript()], | 		plugins: [resolve(), typescript()], | ||||||
| 	}, | 	}, | ||||||
|  | 	{ | ||||||
|  | 		input: 'dist/utils/index.js', | ||||||
|  | 		output: { | ||||||
|  | 			file: 'dist/utils.bundle.min.js', | ||||||
|  | 			format: 'esm', | ||||||
|  | 			inlineDynamicImports: true, | ||||||
|  | 			plugins: [terser()], | ||||||
|  | 		}, | ||||||
|  | 		plugins: [resolve(), typescript({ outDir: 'dist' })], | ||||||
|  | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		input: 'dist/types/main.d.ts', | 		input: 'dist/types/main.d.ts', | ||||||
| 		output: { | 		output: { | ||||||
| 			file: 'dist/types.d.ts', | 			file: 'dist/types/main.d.ts', | ||||||
| 			format: 'es', | 			format: 'es', | ||||||
| 		}, | 		}, | ||||||
| 		plugins: [dts()], | 		plugins: [dts()], | ||||||
|  |  | ||||||
							
								
								
									
										927
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										927
									
								
								src/main.ts
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										13
									
								
								src/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import initState from './initState' | ||||||
|  | import parseTemplate from './parseTemplate' | ||||||
|  | import processTemplateMacros from './processTemplateMarcos' | ||||||
|  | import setupArrowFunctionHandler from './setupArrowFunctionHandler' | ||||||
|  | import triggerDomUpdates from './triggerDomUpdates' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  | 	parseTemplate, | ||||||
|  | 	processTemplateMacros, | ||||||
|  | 	setupArrowFunctionHandler, | ||||||
|  | 	triggerDomUpdates, | ||||||
|  | 	initState, | ||||||
|  | } | ||||||
							
								
								
									
										102
									
								
								src/utils/initState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/utils/initState.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | import triggerDomUpdates from './triggerDomUpdates' | ||||||
|  | 
 | ||||||
|  | export default function initState( | ||||||
|  | 	ops: { | ||||||
|  | 		stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 		textBindings: Array<{ | ||||||
|  | 			node: Text | ||||||
|  | 			expr: string | ||||||
|  | 			originalContent: string | ||||||
|  | 		}> | ||||||
|  | 		attributeBindings: Array<{ | ||||||
|  | 			element: Element | ||||||
|  | 			attrName: string | ||||||
|  | 			expr: string | ||||||
|  | 			template: string | ||||||
|  | 		}> | ||||||
|  | 		updateTextNode: (node: Text, value: string) => void | ||||||
|  | 		getNestedState: (keyPath: string) => unknown | ||||||
|  | 		scheduleUpdate: (elements: Set<HTMLElement>) => void | ||||||
|  | 		conditionalElements: Map< | ||||||
|  | 			Element, | ||||||
|  | 			{ | ||||||
|  | 				expr: string | ||||||
|  | 				placeholder: Comment | ||||||
|  | 				isPresent: boolean | ||||||
|  | 			} | ||||||
|  | 		> | ||||||
|  | 		evaluateIfCondition: (element: Element, expr: string) => void | ||||||
|  | 		currentRenderingElement?: HTMLElement | ||||||
|  | 		statesListenersSelf: Record<string, (...args: unknown[]) => void> | ||||||
|  | 	}, | ||||||
|  | 	states?: Record<string, unknown>, | ||||||
|  | 	statesListeners?: { [key: string]: (value: unknown) => void } | undefined, | ||||||
|  | ) { | ||||||
|  | 	console.log(states) | ||||||
|  | 	// copy state from options
 | ||||||
|  | 	return new Proxy( | ||||||
|  | 		{ ...(states || {}) }, | ||||||
|  | 		{ | ||||||
|  | 			set: ( | ||||||
|  | 				target: Record<string, unknown>, | ||||||
|  | 				keyPath: string, | ||||||
|  | 				value: unknown, | ||||||
|  | 			) => { | ||||||
|  | 				const valueRoute = keyPath.split('.') | ||||||
|  | 				let currentTarget = target | ||||||
|  | 				for (const i in valueRoute) { | ||||||
|  | 					const key = valueRoute[i] | ||||||
|  | 					if (Number.parseInt(i) === valueRoute.length - 1) { | ||||||
|  | 						currentTarget[key] = value | ||||||
|  | 					} else { | ||||||
|  | 						if (!currentTarget[key]) currentTarget[key] = {} | ||||||
|  | 						currentTarget = currentTarget[key] as Record<string, unknown> | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				// trigger dom updates
 | ||||||
|  | 				triggerDomUpdates(keyPath, { | ||||||
|  | 					stateToElementsMap: ops.stateToElementsMap, | ||||||
|  | 					textBindings: ops.textBindings, | ||||||
|  | 					attributeBindings: ops.attributeBindings, | ||||||
|  | 					updateTextNode: ops.updateTextNode, | ||||||
|  | 					getNestedState: ops.getNestedState, | ||||||
|  | 					scheduleUpdate: ops.scheduleUpdate, | ||||||
|  | 				}) | ||||||
|  | 				if (ops.statesListenersSelf[keyPath]) | ||||||
|  | 					ops.statesListenersSelf[keyPath](value) | ||||||
|  | 
 | ||||||
|  | 				// trigger %if macros
 | ||||||
|  | 				if (ops.conditionalElements.size > 0) | ||||||
|  | 					ops.conditionalElements.forEach((info, element) => { | ||||||
|  | 						if (info.expr.includes(keyPath)) | ||||||
|  | 							ops.evaluateIfCondition(element, info.expr) | ||||||
|  | 					}) | ||||||
|  | 
 | ||||||
|  | 				// trigger state update events
 | ||||||
|  | 				statesListeners?.[keyPath]?.(value) | ||||||
|  | 
 | ||||||
|  | 				return true | ||||||
|  | 			}, | ||||||
|  | 			get: (target: Record<string, unknown>, keyPath: string) => { | ||||||
|  | 				// collect state dependencies
 | ||||||
|  | 				if (ops.currentRenderingElement) { | ||||||
|  | 					if (!ops.stateToElementsMap[keyPath]) | ||||||
|  | 						ops.stateToElementsMap[keyPath] = new Set() | ||||||
|  | 					ops.stateToElementsMap[keyPath].add(ops.currentRenderingElement) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				const valueRoute = keyPath.split('.') | ||||||
|  | 				let currentTarget = target | ||||||
|  | 				for (const i in valueRoute) { | ||||||
|  | 					const key = valueRoute[i] | ||||||
|  | 					if (Number.parseInt(i) === valueRoute.length - 1) | ||||||
|  | 						return currentTarget[key] | ||||||
|  | 
 | ||||||
|  | 					if (!currentTarget[key]) currentTarget[key] = {} | ||||||
|  | 					currentTarget = currentTarget[key] as Record<string, unknown> | ||||||
|  | 				} | ||||||
|  | 				return undefined | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/utils/parseTemplate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/utils/parseTemplate.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | export default function parseTemplate(template: string): Element { | ||||||
|  | 	const parser = new DOMParser() | ||||||
|  | 	const doc = parser.parseFromString(template, 'text/html') | ||||||
|  | 
 | ||||||
|  | 	const mainContent = doc.body.firstElementChild | ||||||
|  | 	let rootElement: Element | ||||||
|  | 
 | ||||||
|  | 	if (mainContent) rootElement = document.importNode(mainContent, true) | ||||||
|  | 	else { | ||||||
|  | 		const container = document.createElement('div') | ||||||
|  | 		container.innerHTML = template | ||||||
|  | 		rootElement = container | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return rootElement | ||||||
|  | } | ||||||
							
								
								
									
										755
									
								
								src/utils/processTemplateMarcos.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										755
									
								
								src/utils/processTemplateMarcos.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,755 @@ | ||||||
|  | import setupArrowFunctionHandler from './setupArrowFunctionHandler' | ||||||
|  | 
 | ||||||
|  | interface CustomElement extends HTMLElement { | ||||||
|  | 	setState(key_path: string, value: unknown): void | ||||||
|  | 	getState(key_path: string): unknown | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ListRenderingContext { | ||||||
|  | 	states: Record<string, unknown> | ||||||
|  | 	stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 	statesListeners: Record<string, (value: unknown) => void> | ||||||
|  | 	setState: (keyPath: string, value: unknown) => void | ||||||
|  | 	getState: (keyPath: string) => unknown | ||||||
|  | 	triggerFunc: (eventName: string, ...args: unknown[]) => void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function processTemplateMacros( | ||||||
|  | 	element: Element, | ||||||
|  | 	context: CustomElement, | ||||||
|  | 	options: { | ||||||
|  | 		updateTextNode: (node: Text, expr: string, originalContent: string) => void | ||||||
|  | 		setupAttributeBinding: ( | ||||||
|  | 			element: Element, | ||||||
|  | 			attrName: string, | ||||||
|  | 			expr: string, | ||||||
|  | 			attrValue: string, | ||||||
|  | 		) => void | ||||||
|  | 		setupFunctionCallHandler: ( | ||||||
|  | 			element: Element, | ||||||
|  | 			eventName: string, | ||||||
|  | 			handlerValue: string, | ||||||
|  | 		) => void | ||||||
|  | 		setupExpressionHandler: ( | ||||||
|  | 			element: Element, | ||||||
|  | 			eventName: string, | ||||||
|  | 			handlerValue: string, | ||||||
|  | 		) => void | ||||||
|  | 		stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 		textBindings: { | ||||||
|  | 			node: Text | ||||||
|  | 			expr: string | ||||||
|  | 			originalContent: string | ||||||
|  | 		}[] | ||||||
|  | 		availableFuncs: string[] | ||||||
|  | 		stateListeners: Record<string, (newValue: unknown) => void> | ||||||
|  | 		conditionalElements: Map< | ||||||
|  | 			Element, | ||||||
|  | 			{ expr: string; placeholder: Comment; isPresent: boolean } | ||||||
|  | 		> | ||||||
|  | 		evaluateIfCondition: (element: Element, expr: string) => void | ||||||
|  | 		extractStatePathsFromExpression: (expr: string) => string[] | ||||||
|  | 		states: Record<string, unknown> | ||||||
|  | 		triggerFunc: (eventName: string, ...args: unknown[]) => void | ||||||
|  | 	}, | ||||||
|  | ) { | ||||||
|  | 	/* | ||||||
|  | 	 * We define that those prefix are available as macros: | ||||||
|  | 	 * - @ means event binding macro, such as @click="handleClick" | ||||||
|  | 	 * - : means dynamic attribute macro, such as :src="imageUrl" | ||||||
|  | 	 * - % means component controlling macro, such as %if="condition", %for="item in items" and %connect="stateName" | ||||||
|  | 	 */ | ||||||
|  | 
 | ||||||
|  | 	// Traverse all child nodes, including text nodes
 | ||||||
|  | 	const walker = document.createTreeWalker( | ||||||
|  | 		element, | ||||||
|  | 		NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, | ||||||
|  | 		null, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Store nodes and expressions that need to be updated
 | ||||||
|  | 	const textBindings: Array<{ | ||||||
|  | 		node: Text | ||||||
|  | 		expr: string | ||||||
|  | 		originalContent: string | ||||||
|  | 	}> = [] | ||||||
|  | 	const ifDirectivesToProcess: Array<{ element: Element; expr: string }> = [] | ||||||
|  | 
 | ||||||
|  | 	// Traverse the DOM tree
 | ||||||
|  | 	let currentNode: Node | null | ||||||
|  | 	let flag = true | ||||||
|  | 	while (flag) { | ||||||
|  | 		currentNode = walker.nextNode() | ||||||
|  | 		if (!currentNode) { | ||||||
|  | 			flag = false | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Handle text nodes
 | ||||||
|  | 		if (currentNode.nodeType === Node.TEXT_NODE) { | ||||||
|  | 			const textContent = currentNode.textContent || '' | ||||||
|  | 			const textNode = currentNode as Text | ||||||
|  | 
 | ||||||
|  | 			// Check if it contains Handlebars expressions {{ xxx }}
 | ||||||
|  | 			if (textContent.includes('{{')) { | ||||||
|  | 				// Save the original content, including expressions
 | ||||||
|  | 				const originalContent = textContent | ||||||
|  | 
 | ||||||
|  | 				// Record nodes and expressions that need to be updated
 | ||||||
|  | 				const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g) | ||||||
|  | 				if (matches) { | ||||||
|  | 					for (const match of matches) { | ||||||
|  | 						// Extract the expression content, removing {{ }} and spaces
 | ||||||
|  | 						const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim() | ||||||
|  | 
 | ||||||
|  | 						// Store the node, expression, and original content for later updates
 | ||||||
|  | 						textBindings.push({ node: textNode, expr, originalContent }) | ||||||
|  | 
 | ||||||
|  | 						// Set the initial value
 | ||||||
|  | 						options.updateTextNode(textNode, expr, originalContent) | ||||||
|  | 
 | ||||||
|  | 						// Add dependency relationship for this state path
 | ||||||
|  | 						if (!options.stateToElementsMap[expr]) | ||||||
|  | 							options.stateToElementsMap[expr] = new Set() | ||||||
|  | 
 | ||||||
|  | 						options.stateToElementsMap[expr].add( | ||||||
|  | 							textNode as unknown as HTMLElement, | ||||||
|  | 						) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Handle element nodes (can extend to handle attribute bindings, etc.)
 | ||||||
|  | 		else if (currentNode.nodeType === Node.ELEMENT_NODE) { | ||||||
|  | 			const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
 | ||||||
|  | 
 | ||||||
|  | 			// Traverse all macro attributes
 | ||||||
|  | 
 | ||||||
|  | 			// Detect :attr="" bindings, such as :src="imageUrl"
 | ||||||
|  | 			for (const attr of Array.from(currentElementNode.attributes)) { | ||||||
|  | 				if (attr.name.startsWith(':')) { | ||||||
|  | 					const attrName = attr.name.substring(1) // Remove ':'
 | ||||||
|  | 					const expr = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 					// Remove the attribute, as it is not a standard HTML attribute
 | ||||||
|  | 					currentElementNode.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 					// Set up attribute binding
 | ||||||
|  | 					options.setupAttributeBinding( | ||||||
|  | 						currentElementNode, | ||||||
|  | 						attrName, | ||||||
|  | 						expr, | ||||||
|  | 						attr.value, | ||||||
|  | 					) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// Process @event bindings, such as @click="handleClick"
 | ||||||
|  | 			const eventBindings = Array.from(currentElementNode.attributes).filter( | ||||||
|  | 				(attr) => attr.name.startsWith('@'), | ||||||
|  | 			) | ||||||
|  | 			// eventBindings.forEach((attr) => {
 | ||||||
|  | 			for (const attr of eventBindings) { | ||||||
|  | 				const eventName = attr.name.substring(1) // Remove '@'
 | ||||||
|  | 				const handlerValue = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 				// Remove the attribute, as it is not a standard HTML attribute
 | ||||||
|  | 				currentElementNode.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 				// Handle different types of event handlers
 | ||||||
|  | 				if (handlerValue.includes('=>')) { | ||||||
|  | 					// Handle arrow function: @click="e => setState('count', count + 1)"
 | ||||||
|  | 					setupArrowFunctionHandler( | ||||||
|  | 						currentElementNode, | ||||||
|  | 						eventName, | ||||||
|  | 						handlerValue, | ||||||
|  | 						{ | ||||||
|  | 							createHandlerContext: (_event: Event, _element: Element) => ({ | ||||||
|  | 								states: options.states, | ||||||
|  | 								stateToElementsMap: options.stateToElementsMap, | ||||||
|  | 								statesListeners: options.stateListeners, | ||||||
|  | 								setState: context.setState.bind(context), | ||||||
|  | 								getState: context.getState.bind(context), | ||||||
|  | 								triggerFunc: options.triggerFunc.bind(context), | ||||||
|  | 							}), | ||||||
|  | 						}, | ||||||
|  | 					) | ||||||
|  | 				} else if (handlerValue.includes('(') && handlerValue.includes(')')) { | ||||||
|  | 					// Handle function call: @click="increment(5)"
 | ||||||
|  | 					options.setupFunctionCallHandler( | ||||||
|  | 						currentElementNode, | ||||||
|  | 						eventName, | ||||||
|  | 						handlerValue, | ||||||
|  | 					) | ||||||
|  | 				} else if ( | ||||||
|  | 					options.availableFuncs.includes(handlerValue) && | ||||||
|  | 					typeof (context as unknown as Record<string, unknown>)[ | ||||||
|  | 						handlerValue | ||||||
|  | 					] === 'function' | ||||||
|  | 				) { | ||||||
|  | 					// Handle method reference: @click="handleClick"
 | ||||||
|  | 					currentElementNode.addEventListener( | ||||||
|  | 						eventName, | ||||||
|  | 						( | ||||||
|  | 							context as unknown as Record<string, (...args: unknown[]) => void> | ||||||
|  | 						)[handlerValue].bind(context), | ||||||
|  | 					) | ||||||
|  | 				} else { | ||||||
|  | 					// Handle simple expression: @click="count++" or @input="name = $event.target.value"
 | ||||||
|  | 					options.setupExpressionHandler( | ||||||
|  | 						currentElementNode, | ||||||
|  | 						eventName, | ||||||
|  | 						handlerValue, | ||||||
|  | 					) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items"
 | ||||||
|  | 			const macroBindings = Array.from(currentElementNode.attributes).filter( | ||||||
|  | 				(attr) => attr.name.startsWith('%'), | ||||||
|  | 			) | ||||||
|  | 
 | ||||||
|  | 			// macroBindings.forEach((attr) => {
 | ||||||
|  | 			for (const attr of macroBindings) { | ||||||
|  | 				const macroName = attr.name.substring(1) // Remove '%'
 | ||||||
|  | 				const expr = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 				// Remove the attribute, as it is not a standard HTML attribute
 | ||||||
|  | 				currentElementNode.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 				// Handle different types of macros
 | ||||||
|  | 				if (macroName === 'connect') | ||||||
|  | 					// Handle state connection: %connect="stateName"
 | ||||||
|  | 					setupTwoWayBinding(currentElementNode, expr, { | ||||||
|  | 						getNestedState: context.getState.bind(context), | ||||||
|  | 						setState: context.setState.bind(context), | ||||||
|  | 						statesListeners: options.stateListeners, | ||||||
|  | 					}) | ||||||
|  | 				else if (macroName === 'if') { | ||||||
|  | 					ifDirectivesToProcess.push({ element: currentElementNode, expr }) | ||||||
|  | 				} else if (macroName === 'for') { | ||||||
|  | 					const listContext: ListRenderingContext = { | ||||||
|  | 						states: options.states, | ||||||
|  | 						stateToElementsMap: options.stateToElementsMap, | ||||||
|  | 						statesListeners: options.stateListeners, | ||||||
|  | 						setState: context.setState.bind(context), | ||||||
|  | 						getState: context.getState.bind(context), | ||||||
|  | 						triggerFunc: options.triggerFunc.bind(context), | ||||||
|  | 					} | ||||||
|  | 					setupListRendering(currentElementNode, expr, listContext) | ||||||
|  | 				} else if (macroName === 'key') continue | ||||||
|  | 				else console.warn(`Unknown macro: %${macroName}`) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Save text binding relationships for updates
 | ||||||
|  | 	options.textBindings = textBindings | ||||||
|  | 
 | ||||||
|  | 	// Process all collected %if directives after the main traversal
 | ||||||
|  | 	for (const { element: ifElement, expr } of ifDirectivesToProcess) { | ||||||
|  | 		setupConditionRendering(ifElement, expr, { | ||||||
|  | 			conditionalElements: options.conditionalElements, | ||||||
|  | 			evaluateIfCondition: options.evaluateIfCondition.bind(context), | ||||||
|  | 			extractStatePathsFromExpression: options.extractStatePathsFromExpression, | ||||||
|  | 			stateToElementsMap: options.stateToElementsMap, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle two-way data binding (%connect macro)
 | ||||||
|  | function setupTwoWayBinding( | ||||||
|  | 	element: Element, | ||||||
|  | 	expr: string, | ||||||
|  | 	ops: { | ||||||
|  | 		getNestedState: (path: string) => unknown | ||||||
|  | 		setState: (path: string, value: unknown) => void | ||||||
|  | 		statesListeners: Record<string, (newValue: unknown) => void> | ||||||
|  | 	}, | ||||||
|  | ) { | ||||||
|  | 	// Get the initial value
 | ||||||
|  | 	const value = ops.getNestedState(expr) | ||||||
|  | 
 | ||||||
|  | 	// Set the initial value
 | ||||||
|  | 	if (value !== undefined) | ||||||
|  | 		element.setAttribute('data-laterano-connect', String(value)) | ||||||
|  | 	else | ||||||
|  | 		console.error( | ||||||
|  | 			`State \`${expr}\` not found in the component state. Although Laterano will try to work with it, it may has potentially unexpected behavior.`, | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 	// Add event listener for input events
 | ||||||
|  | 	element.addEventListener('input', (event: Event) => { | ||||||
|  | 		const target = event.target as HTMLInputElement | ||||||
|  | 		const newValue = target.value | ||||||
|  | 
 | ||||||
|  | 		// Update the state
 | ||||||
|  | 		ops.setState(expr, newValue) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// Add event listener for state changes
 | ||||||
|  | 	ops.statesListeners[expr] = (newValue: unknown) => { | ||||||
|  | 		if (element instanceof HTMLInputElement) element.value = newValue as string | ||||||
|  | 		else element.setAttribute('data-laterano-connect', String(newValue)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle condition rendering (%if macro)
 | ||||||
|  | function setupConditionRendering( | ||||||
|  | 	element: Element, | ||||||
|  | 	expr: string, | ||||||
|  | 	ops: { | ||||||
|  | 		conditionalElements: Map< | ||||||
|  | 			Element, | ||||||
|  | 			{ expr: string; placeholder: Comment; isPresent: boolean } | ||||||
|  | 		> | ||||||
|  | 		evaluateIfCondition: (element: Element, expr: string) => void | ||||||
|  | 		extractStatePathsFromExpression: (expr: string) => string[] | ||||||
|  | 		stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 	}, | ||||||
|  | ) { | ||||||
|  | 	const placeholder = document.createComment(` %if: ${expr} `) | ||||||
|  | 	element.parentNode?.insertBefore(placeholder, element) | ||||||
|  | 
 | ||||||
|  | 	ops.conditionalElements.set(element, { | ||||||
|  | 		expr, | ||||||
|  | 		placeholder, | ||||||
|  | 		isPresent: true, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	ops.evaluateIfCondition(element, expr) | ||||||
|  | 
 | ||||||
|  | 	const statePaths = ops.extractStatePathsFromExpression(expr) | ||||||
|  | 	for (const path of statePaths) { | ||||||
|  | 		if (!ops.stateToElementsMap[path]) ops.stateToElementsMap[path] = new Set() | ||||||
|  | 		ops.stateToElementsMap[path].add(element as HTMLElement) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Evaluate expressions using the item context
 | ||||||
|  | function evaluateExpressionWithItemContext( | ||||||
|  | 	expression: string, | ||||||
|  | 	itemContext: Record<string, unknown>, | ||||||
|  | 	context: ListRenderingContext, | ||||||
|  | 	index?: number, | ||||||
|  | 	itemVar?: string, | ||||||
|  | 	indexVar?: string, | ||||||
|  | ): unknown { | ||||||
|  | 	try { | ||||||
|  | 		// Check if the expression directly references the item variable
 | ||||||
|  | 		if (itemVar && expression === itemVar) { | ||||||
|  | 			return itemContext[itemVar] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Check if the expression is an item property path
 | ||||||
|  | 		if (itemVar && expression.startsWith(`${itemVar}.`)) { | ||||||
|  | 			const propertyPath = expression.substring(itemVar.length + 1) | ||||||
|  | 			const parts = propertyPath.split('.') | ||||||
|  | 			let value = itemContext[itemVar] | ||||||
|  | 
 | ||||||
|  | 			for (const part of parts) { | ||||||
|  | 				if (value === undefined || value === null) return undefined | ||||||
|  | 				value = (value as { [key: string]: unknown })[part] | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return value | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Check if the expression directly references the index variable
 | ||||||
|  | 		if (indexVar && expression === indexVar) { | ||||||
|  | 			return index | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Create a merged context (component state + item context)
 | ||||||
|  | 		const mergedContext = { ...context.states, ...itemContext } | ||||||
|  | 
 | ||||||
|  | 		// Create a function to evaluate the expression
 | ||||||
|  | 		const contextKeys = Object.keys(mergedContext) | ||||||
|  | 		const contextValues = Object.values(mergedContext) | ||||||
|  | 
 | ||||||
|  | 		// Use the with statement to allow the expression to access all properties in the context
 | ||||||
|  | 		const func = new Function(...contextKeys, `return ${expression}`) | ||||||
|  | 		return func(...contextValues) | ||||||
|  | 	} catch (error) { | ||||||
|  | 		console.error( | ||||||
|  | 			`Error evaluating expression with item context: ${expression}`, | ||||||
|  | 			error, | ||||||
|  | 		) | ||||||
|  | 		return undefined | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Set up nested list rendering
 | ||||||
|  | function setupNestedListRendering( | ||||||
|  | 	element: Element, | ||||||
|  | 	expr: string, | ||||||
|  | 	parentItemContext: Record<string, unknown>, | ||||||
|  | 	context: ListRenderingContext, | ||||||
|  | ) { | ||||||
|  | 	// Parse the expression (e.g., "subItem in item.subItems")
 | ||||||
|  | 	const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/) | ||||||
|  | 	if (!match) { | ||||||
|  | 		console.error(`Invalid nested %for expression: ${expr}`) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Extract the item variable name, index variable name (optional), and collection expression
 | ||||||
|  | 	const itemVar = match[3] || match[1] | ||||||
|  | 	const indexVar = match[2] || null | ||||||
|  | 	const collectionExpr = match[4].trim() | ||||||
|  | 
 | ||||||
|  | 	// Evaluate the collection expression, using the parent item context
 | ||||||
|  | 	const collection = evaluateExpressionWithItemContext( | ||||||
|  | 		collectionExpr, | ||||||
|  | 		parentItemContext, | ||||||
|  | 		context, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if (!collection || !Array.isArray(collection)) { | ||||||
|  | 		console.warn( | ||||||
|  | 			`Nested collection "${collectionExpr}" is not an array or does not exist`, | ||||||
|  | 		) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create a placeholder comment
 | ||||||
|  | 	const placeholder = document.createComment(` %for: ${expr} `) | ||||||
|  | 	element.parentNode?.insertBefore(placeholder, element) | ||||||
|  | 
 | ||||||
|  | 	// Remove the original template element from the DOM
 | ||||||
|  | 	const template = element.cloneNode(true) as Element | ||||||
|  | 	element.parentNode?.removeChild(element) | ||||||
|  | 
 | ||||||
|  | 	// Create an element for each item
 | ||||||
|  | 	collection.forEach((item, index) => { | ||||||
|  | 		const itemElement = template.cloneNode(true) as Element | ||||||
|  | 
 | ||||||
|  | 		// Create a nested item context, merging the parent context
 | ||||||
|  | 		const nestedItemContext = { | ||||||
|  | 			...parentItemContext, | ||||||
|  | 			[itemVar]: item, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (indexVar) { | ||||||
|  | 			nestedItemContext[indexVar] = index | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Recursively process this item and its children
 | ||||||
|  | 		processElementWithItemContext(itemElement, nestedItemContext, context) | ||||||
|  | 
 | ||||||
|  | 		// Insert the item element into the DOM
 | ||||||
|  | 		placeholder.parentNode?.insertBefore(itemElement, placeholder.nextSibling) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Recursively process the element and its children, applying the item context
 | ||||||
|  | function processElementWithItemContext( | ||||||
|  | 	element: Element, | ||||||
|  | 	itemContext: Record<string, unknown>, | ||||||
|  | 	context: ListRenderingContext, | ||||||
|  | ) { | ||||||
|  | 	// Store the item context of the element so that subsequent updates can find it
 | ||||||
|  | 	;(element as { _itemContext?: Record<string, unknown> })._itemContext = | ||||||
|  | 		itemContext | ||||||
|  | 
 | ||||||
|  | 	// Process bindings in text nodes
 | ||||||
|  | 	const processTextNodes = (node: Node) => { | ||||||
|  | 		if (node.nodeType === Node.TEXT_NODE) { | ||||||
|  | 			const textContent = node.textContent || '' | ||||||
|  | 			if (textContent.includes('{{')) { | ||||||
|  | 				const textNode = node as Text | ||||||
|  | 				const updatedContent = textContent.replace( | ||||||
|  | 					/\{\{\s*([^}]+)\s*\}\}/g, | ||||||
|  | 					(match, expr) => { | ||||||
|  | 						const value = evaluateExpressionWithItemContext( | ||||||
|  | 							expr.trim(), | ||||||
|  | 							itemContext, | ||||||
|  | 							context, | ||||||
|  | 						) | ||||||
|  | 						return value !== undefined ? String(value) : '' | ||||||
|  | 					}, | ||||||
|  | 				) | ||||||
|  | 				textNode.textContent = updatedContent | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Process the text nodes of the element itself
 | ||||||
|  | 	for (const node of Array.from(element.childNodes)) | ||||||
|  | 		if (node.nodeType === Node.TEXT_NODE) processTextNodes(node) | ||||||
|  | 
 | ||||||
|  | 	// Process attribute bindings (:attr)
 | ||||||
|  | 	for (const attr of Array.from(element.attributes)) { | ||||||
|  | 		if (attr.name.startsWith(':')) { | ||||||
|  | 			const attrName = attr.name.substring(1) | ||||||
|  | 			const expr = attr.value.trim() | ||||||
|  | 			const value = evaluateExpressionWithItemContext( | ||||||
|  | 				expr, | ||||||
|  | 				itemContext, | ||||||
|  | 				context, | ||||||
|  | 			) | ||||||
|  | 
 | ||||||
|  | 			if (value !== undefined) element.setAttribute(attrName, String(value)) | ||||||
|  | 
 | ||||||
|  | 			// Remove the original binding attribute
 | ||||||
|  | 			element.removeAttribute(attr.name) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Process event bindings (@event)
 | ||||||
|  | 	for (const attr of Array.from(element.attributes)) { | ||||||
|  | 		if (attr.name.startsWith('@')) { | ||||||
|  | 			const eventName = attr.name.substring(1) | ||||||
|  | 			const handlerValue = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 			// Remove the original binding attribute
 | ||||||
|  | 			element.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 			// Add event listener
 | ||||||
|  | 			element.addEventListener(eventName, (event: Event) => { | ||||||
|  | 				try { | ||||||
|  | 					// Create a merged context
 | ||||||
|  | 					const mergedContext = { | ||||||
|  | 						...context.states, | ||||||
|  | 						...itemContext, | ||||||
|  | 						$event: event, | ||||||
|  | 						$el: element, | ||||||
|  | 						setState: context.setState, | ||||||
|  | 						getState: context.getState, | ||||||
|  | 						triggerFunc: (eventName: string, ...args: unknown[]) => | ||||||
|  | 							context.triggerFunc(eventName, ...args), | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					// Execute the expression
 | ||||||
|  | 					const fnStr = `with(this) { ${handlerValue} }` | ||||||
|  | 					new Function(fnStr).call(mergedContext) | ||||||
|  | 				} catch (err) { | ||||||
|  | 					console.error( | ||||||
|  | 						`Error executing event handler with item context: ${handlerValue}`, | ||||||
|  | 						err, | ||||||
|  | 					) | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Process conditional rendering (%if)
 | ||||||
|  | 	let isConditional = false | ||||||
|  | 	let shouldDisplay = true | ||||||
|  | 
 | ||||||
|  | 	for (const attr of Array.from(element.attributes)) { | ||||||
|  | 		if (attr.name === '%if') { | ||||||
|  | 			isConditional = true | ||||||
|  | 			const expr = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 			// Remove the original binding attribute
 | ||||||
|  | 			element.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 			// Calculate the condition
 | ||||||
|  | 			const result = evaluateExpressionWithItemContext( | ||||||
|  | 				expr, | ||||||
|  | 				itemContext, | ||||||
|  | 				context, | ||||||
|  | 			) | ||||||
|  | 			shouldDisplay = Boolean(result) | ||||||
|  | 
 | ||||||
|  | 			// Apply the condition
 | ||||||
|  | 			if (!shouldDisplay) (element as HTMLElement).style.display = 'none' | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If the condition evaluates to false, skip further processing of this element
 | ||||||
|  | 	if (isConditional && !shouldDisplay) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Process nested list rendering (%for)
 | ||||||
|  | 	let hasForDirective = false | ||||||
|  | 
 | ||||||
|  | 	for (const attr of Array.from(element.attributes)) { | ||||||
|  | 		if (attr.name === '%for') { | ||||||
|  | 			hasForDirective = true | ||||||
|  | 			const forExpr = attr.value.trim() | ||||||
|  | 
 | ||||||
|  | 			// Remove the original binding attribute
 | ||||||
|  | 			element.removeAttribute(attr.name) | ||||||
|  | 
 | ||||||
|  | 			// Set up nested list rendering
 | ||||||
|  | 			setupNestedListRendering(element, forExpr, itemContext, context) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If this element is a list element, skip child element processing
 | ||||||
|  | 	if (hasForDirective) return | ||||||
|  | 
 | ||||||
|  | 	// Recursively process all child elements
 | ||||||
|  | 	for (const child of Array.from(element.children)) | ||||||
|  | 		processElementWithItemContext(child, itemContext, context) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle list rendering (%for macro)
 | ||||||
|  | function setupListRendering( | ||||||
|  | 	element: Element, | ||||||
|  | 	expr: string, | ||||||
|  | 	context: ListRenderingContext, | ||||||
|  | ) { | ||||||
|  | 	// Parse the expression (e.g., "item in items" or "(item, index) in items")
 | ||||||
|  | 	const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/) | ||||||
|  | 	if (!match) { | ||||||
|  | 		console.error(`Invalid %for expression: ${expr}`) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Extract the item variable name, index variable name (optional), and collection expression
 | ||||||
|  | 	const itemVar = match[3] || match[1] | ||||||
|  | 	const indexVar = match[2] || null | ||||||
|  | 	const collectionExpr = match[4].trim() | ||||||
|  | 
 | ||||||
|  | 	// Create a placeholder comment
 | ||||||
|  | 	const placeholder = document.createComment(` %for: ${expr} `) | ||||||
|  | 	element.parentNode?.insertBefore(placeholder, element) | ||||||
|  | 
 | ||||||
|  | 	// Remove the original template element from the DOM
 | ||||||
|  | 	const template = element.cloneNode(true) as Element | ||||||
|  | 	element.parentNode?.removeChild(element) | ||||||
|  | 
 | ||||||
|  | 	// Store current rendered items
 | ||||||
|  | 	const renderedItems: Array<{ | ||||||
|  | 		element: Element | ||||||
|  | 		key: unknown | ||||||
|  | 		data: unknown | ||||||
|  | 		index: number | ||||||
|  | 	}> = [] | ||||||
|  | 
 | ||||||
|  | 	// Create a function to update the list when the collection changes
 | ||||||
|  | 	const updateList = () => { | ||||||
|  | 		const collection = evaluateExpressionWithItemContext( | ||||||
|  | 			collectionExpr, | ||||||
|  | 			{}, | ||||||
|  | 			context, | ||||||
|  | 		) | ||||||
|  | 		if (!collection || !Array.isArray(collection)) { | ||||||
|  | 			console.warn( | ||||||
|  | 				`Collection "${collectionExpr}" is not an array or does not exist`, | ||||||
|  | 			) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const parentNode = placeholder.parentNode | ||||||
|  | 		if (!parentNode) { | ||||||
|  | 			console.error("Placeholder's parentNode is null. Cannot update list.") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Detach all currently rendered DOM items
 | ||||||
|  | 		for (const item of renderedItems) | ||||||
|  | 			if (item.element.parentNode === parentNode) | ||||||
|  | 				parentNode.removeChild(item.element) | ||||||
|  | 
 | ||||||
|  | 		// Get key attribute if available
 | ||||||
|  | 		const keyAttr = template.getAttribute('%key') | ||||||
|  | 		if (!keyAttr) | ||||||
|  | 			console.warn( | ||||||
|  | 				'%key attribute not found in the template, which is not a recommended practice.', | ||||||
|  | 			) | ||||||
|  | 
 | ||||||
|  | 		// Store a map of existing items by key for reuse
 | ||||||
|  | 		const existingElementsByKey = new Map() | ||||||
|  | 		for (const item of renderedItems) | ||||||
|  | 			if (item.key !== undefined) existingElementsByKey.set(item.key, item) | ||||||
|  | 
 | ||||||
|  | 		// Clear rendered items
 | ||||||
|  | 		renderedItems.length = 0 | ||||||
|  | 
 | ||||||
|  | 		// document fragment
 | ||||||
|  | 		const fragment = document.createDocumentFragment() | ||||||
|  | 
 | ||||||
|  | 		// Create or update items in the list
 | ||||||
|  | 		collection.forEach((item, index) => { | ||||||
|  | 			// Determine the key for this item
 | ||||||
|  | 			const key = keyAttr | ||||||
|  | 				? evaluateExpressionWithItemContext( | ||||||
|  | 						keyAttr, | ||||||
|  | 						{ [itemVar]: item }, | ||||||
|  | 						context, | ||||||
|  | 						index, | ||||||
|  | 						itemVar, | ||||||
|  | 						indexVar ? indexVar : undefined, | ||||||
|  | 					) | ||||||
|  | 				: index | ||||||
|  | 
 | ||||||
|  | 			// Check if we can reuse an existing element
 | ||||||
|  | 			const existingItem = existingElementsByKey.get(key) | ||||||
|  | 			let itemElement: Element | ||||||
|  | 
 | ||||||
|  | 			if (existingItem) { | ||||||
|  | 				// Reuse existing element
 | ||||||
|  | 				itemElement = existingItem.element | ||||||
|  | 				existingElementsByKey.delete(key) // Remove from map so we know it's been used
 | ||||||
|  | 			} else { | ||||||
|  | 				// Create a new element
 | ||||||
|  | 				itemElement = template.cloneNode(true) as Element | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// Update item data
 | ||||||
|  | 			renderedItems.push({ | ||||||
|  | 				element: itemElement, | ||||||
|  | 				key, | ||||||
|  | 				data: item, | ||||||
|  | 				index, | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			// Create item context for this item
 | ||||||
|  | 			const itemContext = { | ||||||
|  | 				[itemVar]: item, | ||||||
|  | 			} | ||||||
|  | 			if (indexVar) itemContext[indexVar] = index | ||||||
|  | 
 | ||||||
|  | 			// insert %key attribute
 | ||||||
|  | 			if (keyAttr) { | ||||||
|  | 				const keyValue = evaluateExpressionWithItemContext( | ||||||
|  | 					keyAttr, | ||||||
|  | 					itemContext, | ||||||
|  | 					context, | ||||||
|  | 				) | ||||||
|  | 				itemElement.setAttribute('data-laterano-key', String(keyValue)) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// remove original %key attribute
 | ||||||
|  | 			itemElement.removeAttribute('%key') | ||||||
|  | 
 | ||||||
|  | 			// Process the element with the item context
 | ||||||
|  | 			processElementWithItemContext(itemElement, itemContext, context) | ||||||
|  | 
 | ||||||
|  | 			// Insert the element to the document fragment
 | ||||||
|  | 			fragment.appendChild(itemElement) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		// Insert the document fragment into the DOM
 | ||||||
|  | 		placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling) | ||||||
|  | 
 | ||||||
|  | 		// Remove any remaining unused items
 | ||||||
|  | 		for (const item of existingElementsByKey.values()) | ||||||
|  | 			if (item.element.parentNode) | ||||||
|  | 				item.element.parentNode.removeChild(item.element) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Initial render
 | ||||||
|  | 	updateList() | ||||||
|  | 
 | ||||||
|  | 	// Set up state dependency for collection changes
 | ||||||
|  | 	if (!context.stateToElementsMap[collectionExpr]) | ||||||
|  | 		context.stateToElementsMap[collectionExpr] = new Set() | ||||||
|  | 
 | ||||||
|  | 	// Using a unique identifier for this list rendering instance
 | ||||||
|  | 	const listVirtualElement = document.createElement('div') | ||||||
|  | 	context.stateToElementsMap[collectionExpr].add( | ||||||
|  | 		listVirtualElement as HTMLElement, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Add listener for state changes
 | ||||||
|  | 	context.statesListeners[collectionExpr] = () => { | ||||||
|  | 		updateList() | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								src/utils/setupArrowFunctionHandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/utils/setupArrowFunctionHandler.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | ||||||
|  | export default function setupArrowFunctionHandler( | ||||||
|  | 	element: Element, | ||||||
|  | 	eventName: string, | ||||||
|  | 	handlerValue: string, | ||||||
|  | 	ops: { | ||||||
|  | 		createHandlerContext: ( | ||||||
|  | 			event: Event, | ||||||
|  | 			element: Element, | ||||||
|  | 		) => { | ||||||
|  | 			states: Record<string, unknown> | ||||||
|  | 			stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 			statesListeners: Record<string, (value: unknown) => void> | ||||||
|  | 			setState: (keyPath: string, value: unknown) => void | ||||||
|  | 			getState: (keyPath: string) => unknown | ||||||
|  | 			triggerFunc: (eventName: string, ...args: unknown[]) => void | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | ) { | ||||||
|  | 	element.addEventListener(eventName, (event: Event) => { | ||||||
|  | 		try { | ||||||
|  | 			// Arrow function parsing
 | ||||||
|  | 			const splitted = handlerValue.split('=>') | ||||||
|  | 			if (splitted.length !== 2) { | ||||||
|  | 				throw new Error(`Invalid arrow function syntax: ${handlerValue}`) | ||||||
|  | 			} | ||||||
|  | 			const paramsStr = (() => { | ||||||
|  | 				if (splitted[0].includes('(')) return splitted[0].trim() | ||||||
|  | 				return `(${splitted[0].trim()})` | ||||||
|  | 			})() | ||||||
|  | 			const bodyStr = splitted[1].trim() | ||||||
|  | 
 | ||||||
|  | 			// Check if the function body is wrapped in {}
 | ||||||
|  | 			const isMultiline = bodyStr.startsWith('{') && bodyStr.endsWith('}') | ||||||
|  | 
 | ||||||
|  | 			// If it is a multiline function body, remove the outer braces
 | ||||||
|  | 			if (isMultiline) { | ||||||
|  | 				// Remove the outer braces
 | ||||||
|  | 				let bodyStr = handlerValue.split('=>')[1].trim() | ||||||
|  | 				bodyStr = bodyStr.substring(1, bodyStr.length - 1) | ||||||
|  | 
 | ||||||
|  | 				// Build code for multiline arrow function
 | ||||||
|  | 				const functionCode = ` | ||||||
|  | 									return function${paramsStr} { | ||||||
|  | 										${bodyStr} | ||||||
|  | 									} | ||||||
|  | 								` | ||||||
|  | 
 | ||||||
|  | 				// Create context object
 | ||||||
|  | 				const context = ops.createHandlerContext(event, element) | ||||||
|  | 
 | ||||||
|  | 				// Create and call function
 | ||||||
|  | 				const handlerFn = new Function(functionCode).call(null) | ||||||
|  | 				handlerFn.apply(context, [event]) | ||||||
|  | 			} else { | ||||||
|  | 				// Single line arrow function, directly return expression result
 | ||||||
|  | 				const functionCode = ` | ||||||
|  | 									return function${paramsStr} { | ||||||
|  | 										return ${bodyStr} | ||||||
|  | 									} | ||||||
|  | 								` | ||||||
|  | 
 | ||||||
|  | 				// Create context object
 | ||||||
|  | 				const context = ops.createHandlerContext(event, element) | ||||||
|  | 
 | ||||||
|  | 				// Create and call function
 | ||||||
|  | 				const handlerFn = new Function(functionCode).call(null) | ||||||
|  | 				handlerFn.apply(context, [event]) | ||||||
|  | 			} | ||||||
|  | 		} catch (err) { | ||||||
|  | 			console.error( | ||||||
|  | 				`Error executing arrow function handler: ${handlerValue}`, | ||||||
|  | 				err, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								src/utils/triggerDomUpdates.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/utils/triggerDomUpdates.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | export default function triggerDomUpdates( | ||||||
|  | 	keyPath: string, | ||||||
|  | 	ops: { | ||||||
|  | 		stateToElementsMap: Record<string, Set<HTMLElement>> | ||||||
|  | 		scheduleUpdate: (elements: Set<HTMLElement>) => void | ||||||
|  | 		textBindings: | ||||||
|  | 			| Array<{ | ||||||
|  | 					node: Text | ||||||
|  | 					expr: string | ||||||
|  | 					originalContent: string | ||||||
|  | 			  }> | ||||||
|  | 			| undefined | ||||||
|  | 		attributeBindings: | ||||||
|  | 			| Array<{ | ||||||
|  | 					element: Element | ||||||
|  | 					attrName: string | ||||||
|  | 					expr: string | ||||||
|  | 					template: string | ||||||
|  | 			  }> | ||||||
|  | 			| undefined | ||||||
|  | 		updateTextNode: (node: Text, expr: string, template: string) => void | ||||||
|  | 		getNestedState: (path: string) => unknown | ||||||
|  | 	}, | ||||||
|  | ) { | ||||||
|  | 	if (ops.stateToElementsMap[keyPath]) { | ||||||
|  | 		const updateQueue = new Set<HTMLElement>() | ||||||
|  | 
 | ||||||
|  | 		for (const element of ops.stateToElementsMap[keyPath]) | ||||||
|  | 			updateQueue.add(element) | ||||||
|  | 
 | ||||||
|  | 		ops.scheduleUpdate(updateQueue) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update text bindings that depend on this state
 | ||||||
|  | 	if (ops.textBindings) { | ||||||
|  | 		// this._textBindings.forEach((binding) => {
 | ||||||
|  | 		for (const binding of ops.textBindings) | ||||||
|  | 			if (binding.expr === keyPath || binding.expr.startsWith(`${keyPath}.`)) | ||||||
|  | 				ops.updateTextNode(binding.node, binding.expr, binding.originalContent) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update attribute bindings that depend on this state
 | ||||||
|  | 	if (ops.attributeBindings) { | ||||||
|  | 		for (const binding of ops.attributeBindings) | ||||||
|  | 			if (binding.expr === keyPath || binding.expr.startsWith(`${keyPath}.`)) { | ||||||
|  | 				const value = ops.getNestedState(binding.expr) | ||||||
|  | 				if (value !== undefined) | ||||||
|  | 					binding.element.setAttribute(binding.attrName, String(value)) | ||||||
|  | 			} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -35,7 +35,9 @@ | ||||||
| 		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */ | 		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */ | ||||||
| 		// "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */ | 		// "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */ | ||||||
| 		// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */ | 		// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */ | ||||||
| 		// "typeRoots": ["./src/types"],                                  /* Specify multiple folders that act like './node_modules/@types'. */ | 		"typeRoots": [ | ||||||
|  | 			"./src/types" | ||||||
|  | 		] /* Specify multiple folders that act like './node_modules/@types'. */, | ||||||
| 		// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */ | 		// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */ | ||||||
| 		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */ | 		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */ | ||||||
| 		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */ | 		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user