Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(new tool): CSS <> XPath converter #1256

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ declare module '@vue/runtime-core' {
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
CssSelectorsMemo: typeof import('./src/tools/css-selectors-memo/css-selectors-memo.md')['default']
CssXpathConverter: typeof import('./src/tools/css-xpath-converter/css-xpath-converter.vue')['default']
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
Expand Down Expand Up @@ -188,6 +190,7 @@ declare module '@vue/runtime-core' {
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
XmlToJson: typeof import('./src/tools/xml-to-json/xml-to-json.vue')['default']
XpathMemo: typeof import('./src/tools/xpath-memo/xpath-memo.md')['default']
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
YamlViewer: typeof import('./src/tools/yaml-viewer/yaml-viewer.vue')['default']
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"cron-validator": "^1.3.1",
"cronstrue": "^2.26.0",
"crypto-js": "^4.1.1",
"csstoxpath": "^2.0.0",
"date-fns": "^2.29.3",
"dompurify": "^3.0.6",
"email-normalizer": "^1.0.0",
Expand Down Expand Up @@ -102,6 +103,7 @@
"vuedraggable": "^4.1.0",
"xml-formatter": "^3.3.2",
"xml-js": "^1.6.11",
"xpath-to-css": "^1.2.0",
"yaml": "^2.2.1"
},
"devDependencies": {
Expand Down
19 changes: 18 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions src/tools/css-selectors-memo/css-selectors-memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
| Selector | Example | Example description |
|-----------------------|-------------------------|----------------------------------------------------------------------------------------------------------|
| `.class` | `.intro` | Selects all elements with class="intro" |
| `.class1.class2` | `.name1.name2` | Selects all elements with both name1 and name2 set within its class attribute |
| `.class1 .class2` | `.name1 .name2` | Selects all elements with name2 that is a descendant of an element with name1 |
| `#id` | `#firstname` | Selects the element with id="firstname" |
| `*` | `*` | Selects all elements |
| `element` | `p` | Selects all \<p\> elements |
| `element.class` | `p.intro` | Selects all \<p\> elements with class="intro" |
| `element,element` | `div, p` | Selects all \<div\> elements and all \<p\> elements |
| `element element` | `div p` | Selects all \<p\> elements inside \<div\> elements |
| `element>element` | `div > p` | Selects all \<p\> elements where the parent is a \<div\> element |
| `element+element` | `div + p` | Selects the first \<p\> element that is placed immediately after \<div\> elements |
| `element1~element2` | `p ~ ul` | Selects every \<ul\> element that is preceded by a \<p\> element |
| `[attribute]` | `[target]` | Selects all elements with a target attribute |
| `[attribute=value]` | `[target="_blank"]` | Selects all elements with target="_blank" |
| `[attribute~=value]` | `[title~="flower"]` | Selects all elements with a title attribute containing the word "flower" |
| `[attribute\|=value]` | `[lang\|="en"]` | Selects all elements with a lang attribute value equal to "en" or starting with "en-" |
| `[attribute^=value]` | `a[href^="https"]` | Selects every \<a\> element whose href attribute value begins with "https" |
| `[attribute$=value]` | `a[href$=".pdf"]` | Selects every \<a\> element whose href attribute value ends with ".pdf" |
| `[attribute*=value]` | `a[href*="w3schools"]` | Selects every \<a\> element whose href attribute value contains the substring "w3schools" |
| `:active` | `a:active` | Selects the active link |
| `::after` | `p::after` | Insert something after the content of each \<p\> element |
| `::before` | `p::before` | Insert something before the content of each \<p\> element |
| `:checked` | `input:checked` | Selects every checked \<input\> element |
| `:default` | `input:default` | Selects the default \<input\> element |
| `:disabled` | `input:disabled` | Selects every disabled \<input\> element |
| `:empty` | `p:empty` | Selects every \<p\> element that has no children (including text nodes) |
| `:enabled` | `input:enabled` | Selects every enabled \<input\> element |
| `:first-child` | `p:first-child` | Selects every \<p\> element that is the first child of its parent |
| `::first-letter` | `p::first-letter` | Selects the first letter of every \<p\> element |
| `::first-line` | `p::first-line` | Selects the first line of every \<p\> element |
| `:first-of-type` | `p:first-of-type` | Selects every \<p\> element that is the first \<p\> element of its parent |
| `:focus` | `input:focus` | Selects the input element which has focus |
| `:fullscreen` | `:fullscreen` | Selects the element that is in full-screen mode |
| `:has()` | `h2:has(+p)` | Selects h2 elements that are immediately followed by a p element, and applies the style to h2 |
| `:hover` | `a:hover` | Selects links on mouse over |
| `:in-range` | `input:in-range` | Selects input elements with a value within a specified range |
| `:indeterminate` | `input:indeterminate` | Selects input elements that are in an indeterminate state |
| `:invalid` | `input:invalid` | Selects all input elements with an invalid value |
| `:lang()` | `p:lang(it)` | Selects every \<p\> element with a lang attribute equal to "it" (Italian) |
| `:last-child` | `p:last-child` | Selects every \<p\> element that is the last child of its parent |
| `:last-of-type` | `p:last-of-type` | Selects every \<p\> element that is the last \<p\> element of its parent |
| `:link` | `a:link` | Selects all unvisited links |
| `::marker` | `::marker` | Selects the markers of list items |
| `:not()` | `:not(p)` | Selects every element that is not a \<p\> element |
| `:nth-child()` | `p:nth-child(2)` | Selects every \<p\> element that is the second child of its parent |
| `:nth-last-child()` | `p:nth-last-child(2)` | Selects every \<p\> element that is the second child of its parent, counting from the last child |
| `:nth-last-of-type()` | `p:nth-last-of-type(2)` | Selects every \<p\> element that is the second \<p\> element of its parent, counting from the last child |
| `:nth-of-type()` | `p:nth-of-type(2)` | Selects every \<p\> element that is the second \<p\> element of its parent |
| `:only-of-type` | `p:only-of-type` | Selects every \<p\> element that is the only \<p\> element of its parent |
| `:only-child` | `p:only-child` | Selects every \<p\> element that is the only child of its parent |
| `:optional` | `input:optional` | Selects input elements with no "required" attribute |
| `:out-of-range` | `input:out-of-range` | Selects input elements with a value outside a specified range |
| `::placeholder` | `input::placeholder` | Selects input elements with the "placeholder" attribute specified |
| `:read-only` | `input:read-only` | Selects input elements with the "readonly" attribute specified |
| `:read-write` | `input:read-write` | Selects input elements with the "readonly" attribute NOT specified |
| `:required` | `input:required` | Selects input elements with the "required" attribute specified |
| `:root` | `:root` | Selects the document's root element |
| `::selection` | `::selection` | Selects the portion of an element that is selected by a user |
| `:target` | `#news:target` | Selects the current active #news element (clicked on a URL containing that anchor name) |
| `:valid` | `input:valid` | Selects all input elements with a valid value |
| `:visited` | `a:visited` | Selects all visited links |
32 changes: 32 additions & 0 deletions src/tools/css-selectors-memo/css-selectors-memo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useThemeVars } from 'naive-ui';
import Memo from './css-selectors-memo.md';

const themeVars = useThemeVars();
</script>

<template>
<div>
<Memo style="overflow-x: auto;" />
</div>
</template>

<style lang="less" scoped>
::v-deep(pre) {
margin: 0;
padding: 15px 22px;
background-color: v-bind('themeVars.cardColor');
border-radius: 4px;
overflow: auto;
}
::v-deep(table) {
border-collapse: collapse;
}
::v-deep(table), ::v-deep(td), ::v-deep(th) {
border: 1px solid v-bind('themeVars.textColor1');
padding: 5px;
}
::v-deep(a) {
color: v-bind('themeVars.textColor1');
}
</style>
12 changes: 12 additions & 0 deletions src/tools/css-selectors-memo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BrandCss3 } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'CSS Selectors Cheatsheet',
path: '/css-selectors-memo',
description: 'CSS Selectors Syntax Cheatsheet',
keywords: ['css', 'selectors', 'cheatsheet', 'memo'],
component: () => import('./css-selectors-memo.vue'),
icon: BrandCss3,
createdAt: new Date('2024-08-15'),
});
79 changes: 79 additions & 0 deletions src/tools/css-xpath-converter/css-xpath-converter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup lang="ts">
import xPathToCss from 'xpath-to-css';
import cssToXpath from 'csstoxpath';
import TextareaCopyable from '@/components/TextareaCopyable.vue';

const cssInput = ref('');
const xpathOutput = computed(
() => {
try {
return cssToXpath(cssInput.value);
}
catch (e: any) {
return e.toString();
}
},
);

const xpathInput = ref('');
const cssOutput = computed(
() => {
try {
return xPathToCss(xpathInput.value);
}
catch (e: any) {
return e.toString();
}
},
);
</script>

<template>
<div max-w-600>
<c-card title="CSS to XPath">
<c-input-text
v-model:value="cssInput"
placeholder="Put your CSS selector here..."
label="CSS selector to convert"
raw-text
mb-5
/>

<router-link target="_blank" to="/css-selectors-memo" mb-1 mt-1>
See CSS Selectors Cheatsheet
</router-link>

<n-divider />

<TextareaCopyable
label="XPath expression"
:value="xpathOutput"
readonly
mb-5
/>
</c-card>

<c-card title="XPath to CSS" mt-5>
<c-input-text
v-model:value="xpathInput"
placeholder="Put your XPath expression here..."
label="XPath expression to convert"
raw-text
mb-5
/>

<router-link target="_blank" to="/xpath-memo" mb-1 mt-1>
See XPath Cheatsheet
</router-link>

<n-divider />

<TextareaCopyable
label="CSS Selector"
:value="cssOutput"
readonly
mb-5
/>
</c-card>
</div>
</template>
3 changes: 3 additions & 0 deletions src/tools/css-xpath-converter/csstoxpath.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "csstoxpath" {
export default function cssToXPath(xpath: string): string;
}
12 changes: 12 additions & 0 deletions src/tools/css-xpath-converter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Braces } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'CSS XPath Converter',
path: '/css-xpath-converter',
description: 'Convert CSS selector to/from XPath expression',
keywords: ['css', 'xpath', 'converter'],
component: () => import('./css-xpath-converter.vue'),
icon: Braces,
createdAt: new Date('2024-08-15'),
});
3 changes: 3 additions & 0 deletions src/tools/css-xpath-converter/xpath-to-css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "xpath-to-css" {
export default function xpathToCSS(xpath: string): string;
}
8 changes: 6 additions & 2 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as emailNormalizer } from './email-normalizer';

import { tool as cssXpathConverter } from './css-xpath-converter';
import { tool as cssSelectorsMemo } from './css-selectors-memo';
import { tool as xpathMemo } from './xpath-memo';
import { tool as asciiTextDrawer } from './ascii-text-drawer';

import { tool as textToUnicode } from './text-to-unicode';
import { tool as safelinkDecoder } from './safelink-decoder';
import { tool as xmlToJson } from './xml-to-json';
Expand Down Expand Up @@ -158,6 +159,9 @@ export const toolsByCategory: ToolCategory[] = [
xmlFormatter,
yamlViewer,
emailNormalizer,
cssXpathConverter,
cssSelectorsMemo,
xpathMemo,
regexTester,
regexMemo,
],
Expand Down
12 changes: 12 additions & 0 deletions src/tools/xpath-memo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Brackets } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'XPath Syntax Cheatsheet',
path: '/xpath-memo',
description: 'XPath Syntax Cheatsheet',
keywords: ['xpath', 'memo', 'cheatsheet'],
component: () => import('./xpath-memo.vue'),
icon: Brackets,
createdAt: new Date('2024-08-15'),
});
Loading
Loading