├── .eslintignore ├── .npmignore ├── .gitignore ├── test ├── setup-jest.js ├── rules │ ├── niqqud.test.js │ ├── neutral.test.js │ ├── separators.test.js │ ├── boundaries.test.js │ ├── beginnings.test.js │ ├── brackets.test.js │ ├── braces.test.js │ ├── parenthesis.test.js │ ├── predefined.test.js │ ├── unchanged.test.js │ └── endings.test.js ├── utils │ └── finals.test.js ├── otf.test.js ├── ui │ └── accessibility.test.js ├── attributes.test.js └── dom.test.js ├── src ├── ui │ ├── fonts │ │ ├── ivritacons-alefalefalef.eot │ │ ├── ivritacons-alefalefalef.otf │ │ ├── ivritacons-alefalefalef.woff │ │ └── ivritacons-alefalefalef.woff2 │ ├── hebrew.js │ ├── index.js │ ├── docReady.js │ ├── switch.js │ ├── custom.js │ ├── default.js │ └── style.scss ├── utils │ ├── finals.js │ └── characters.js ├── textNode.js ├── textAttribute.js ├── textObject.js ├── textElement.js ├── ivrita.js ├── rules.js ├── wordlists.js └── element.js ├── .babelrc ├── .eslintrc.js ├── .github └── workflows │ ├── test.yml │ └── deploy.yml ├── package.json ├── webpack.config.js ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /test/setup-jest.js: -------------------------------------------------------------------------------- 1 | import jQuery from 'jquery'; 2 | 3 | import 'regenerator-runtime/runtime'; 4 | 5 | global.jQuery = jQuery; 6 | -------------------------------------------------------------------------------- /src/ui/fonts/ivritacons-alefalefalef.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlefAlefAlef/ivrita/HEAD/src/ui/fonts/ivritacons-alefalefalef.eot -------------------------------------------------------------------------------- /src/ui/fonts/ivritacons-alefalefalef.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlefAlefAlef/ivrita/HEAD/src/ui/fonts/ivritacons-alefalefalef.otf -------------------------------------------------------------------------------- /src/ui/fonts/ivritacons-alefalefalef.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlefAlefAlef/ivrita/HEAD/src/ui/fonts/ivritacons-alefalefalef.woff -------------------------------------------------------------------------------- /src/ui/fonts/ivritacons-alefalefalef.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlefAlefAlef/ivrita/HEAD/src/ui/fonts/ivritacons-alefalefalef.woff2 -------------------------------------------------------------------------------- /test/rules/niqqud.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE } from '../../src/ivrita'; 2 | 3 | test('Skip niqqud words', () => { 4 | expect(genderize('מעצּבֻּ/תֻ', FEMALE)).toBe('מעצּבֻּ/תֻ'); 5 | }); 6 | -------------------------------------------------------------------------------- /test/rules/neutral.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, NEUTRAL } from '../../src/ivrita'; 2 | 3 | test('No multiple slashes on neutral', () => { 4 | expect(genderize('הקלד/י', NEUTRAL)).toBe('הקלד/י'); // unchanged 5 | }); 6 | -------------------------------------------------------------------------------- /test/rules/separators.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, MALE } from '../../src/ivrita'; 2 | 3 | test('All separators', () => { 4 | expect(genderize('את/ה', MALE)).toBe('אתה'); 5 | expect(genderize('את.ה', MALE)).toBe('אתה'); 6 | expect(genderize('את\\ה', MALE)).toBe('אתה'); 7 | }); 8 | -------------------------------------------------------------------------------- /src/ui/hebrew.js: -------------------------------------------------------------------------------- 1 | export const iconTitle = 'עבריתה'; 2 | export const aboutLinkText = 'אודות מיזם עבריתה'; 3 | export const defaultMaleLabel = 'איש'; 4 | export const defaultFemaleLabel = 'אישה'; 5 | export const defaultNeutralLabel = 'ניטרלי'; 6 | export const ariaLabel = 'שינוי לשון הפנייה של האתר ל%s'; 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-class-properties", { 7 | "loose": true 8 | }], 9 | ["@babel/plugin-transform-template-literals", { 10 | "loose": true 11 | }], 12 | ["@babel/plugin-transform-react-jsx", { "pragma": "dom" }] 13 | ] 14 | } -------------------------------------------------------------------------------- /src/utils/finals.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | export const fins = { 3 | 'ן': 'נ', 4 | 'ף': 'פ', 5 | 'ך': 'כ', 6 | 'ם': 'מ', 7 | 'ץ': 'צ', 8 | }; 9 | /* eslint-enable quote-props */ 10 | export const finnables = Object.keys(fins).concat(Object.values(fins)); 11 | export const finals = Object.keys(fins); 12 | 13 | export const toFin = (notFin) => Object.keys(fins).find((key) => fins[key] === notFin) || notFin; 14 | export const toNotFin = (fin) => (fins[fin] || fin); 15 | -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | import Ivrita from '../element'; 2 | import CustomSwitch from './custom'; 3 | import DefaultSwitch from './default'; 4 | 5 | import DocReady from './docReady'; 6 | 7 | const defaultSwitch = new DefaultSwitch(); 8 | 9 | export const initDefaultSwitch = () => { 10 | window._ivrita = new Ivrita(); 11 | defaultSwitch.setIvritaInstances(window._ivrita); 12 | defaultSwitch.init(); 13 | }; 14 | 15 | if (typeof document !== 'undefined') { 16 | DocReady(initDefaultSwitch); 17 | } 18 | 19 | defaultSwitch.custom = CustomSwitch; 20 | export default defaultSwitch; 21 | -------------------------------------------------------------------------------- /src/textNode.js: -------------------------------------------------------------------------------- 1 | import TextObject, { IncompatibleTypeError } from './textObject'; 2 | 3 | export default class TextNode extends TextObject { 4 | node = {}; 5 | 6 | constructor(node) { 7 | if (!node) return false; 8 | if (!(node instanceof Text)) { 9 | throw IncompatibleTypeError; 10 | } 11 | 12 | super(node); 13 | 14 | this.node = node; 15 | 16 | if (!this.currentMode) { 17 | this.init(); 18 | } 19 | } 20 | 21 | getValue() { 22 | return this.node.data; 23 | } 24 | 25 | setValue(newVal) { 26 | this.node.data = newVal; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/textAttribute.js: -------------------------------------------------------------------------------- 1 | import TextObject, { IncompatibleTypeError } from './textObject'; 2 | 3 | export default class TextAttribute extends TextObject { 4 | attr = {}; 5 | 6 | constructor(attr) { 7 | if (!attr) return false; 8 | if (!(attr instanceof Attr)) { 9 | throw IncompatibleTypeError; 10 | } 11 | 12 | super(attr); 13 | 14 | this.attr = attr; 15 | 16 | if (!this.currentMode) { 17 | this.init(); 18 | } 19 | } 20 | 21 | getValue() { 22 | return this.attr.value; 23 | } 24 | 25 | setValue(newVal) { 26 | this.attr.value = newVal; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | plugins: ['jest'], 4 | env: { 5 | browser: true, 6 | es2021: true, 7 | 'jest/globals': true, 8 | }, 9 | extends: ['airbnb-base'], 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | sourceType: 'module', 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | globals: { 18 | jQuery: true, 19 | }, 20 | rules: { 21 | 'no-param-reassign': [2, { props: false }], 22 | 'no-new': [0], 23 | 'no-underscore-dangle': 0, 24 | 'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/setup-jest.js'] }], // Allow importing jQuery in Jest setup file 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /test/rules/boundaries.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE, MALE } from '../../src/ivrita'; 2 | 3 | test('Word ending with special chars', () => { 4 | expect(genderize('בוא/י"', FEMALE)).toBe('בואי"'); 5 | expect(genderize('בוא/י"', MALE)).toBe('בוא"'); 6 | 7 | expect(genderize('מרגיש/ה?', FEMALE)).toBe('מרגישה?'); 8 | expect(genderize('מרגיש/ה?', MALE)).toBe('מרגיש?'); 9 | }); 10 | 11 | test('Word beginning with special chars', () => { 12 | expect(genderize('"בוא/י', FEMALE)).toBe('"בואי'); 13 | expect(genderize('"בוא/י', MALE)).toBe('"בוא'); 14 | }); 15 | 16 | test('Rules beginning with boundary', () => { 17 | expect(genderize('י/תכתוב י/תרצה', MALE)).toBe('יכתוב ירצה'); 18 | 19 | expect(genderize('ישראלים/ות סטודנטים/ות', FEMALE)).toBe('ישראליות סטודנטיות'); 20 | }); 21 | -------------------------------------------------------------------------------- /src/ui/docReady.js: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/nickeljew/es6-docready 2 | 3 | export default (callback) => { 4 | function completed() { 5 | document.removeEventListener('DOMContentLoaded', completed, false); 6 | window.removeEventListener('load', completed, false); 7 | callback(); 8 | } 9 | 10 | // Events.on(document, 'DOMContentLoaded', completed) 11 | 12 | if (document.readyState === 'complete') { 13 | // Handle it asynchronously to allow scripts the opportunity to delay ready 14 | setTimeout(callback); 15 | } else { 16 | // Use the handy event callback 17 | document.addEventListener('DOMContentLoaded', completed, false); 18 | 19 | // A fallback to window.onload, that will always work 20 | window.addEventListener('load', completed, false); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/characters.js: -------------------------------------------------------------------------------- 1 | import { finals } from './finals'; 2 | 3 | export const SEP = '[\\\\/.]'; 4 | export const HEB = '[א-ת]'; 5 | export const EXTSEP = '[\\\\./—־-]'; 6 | export const G = '\'‎"”׳״'; // "Gershayim" 7 | export const MAKAF = '—־-'; 8 | export const W = `[א-ת${G}]`; 9 | export const FIN = `[${finals}]`; 10 | 11 | const b = '^|$|\b|[^א-ת\u0590-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C7\u05EF-\u05F2\uFB2A-\uFB4F]'; // Boundary, like "\b" in regex. All unicode characters which can be part of a hebrew word 12 | export const B = `(?=${b})`; // Look-ahead positive 13 | export const CB = `(${b})`; // Just capturing 14 | export const LBPB = `(?<=${b})`; // Look-behind positive - not used since it's not supported by Safari :( 15 | export const NCB = `(?:${b})`; // Non-capturing 16 | 17 | export const SYNTAX = [EXTSEP, '\\[', '\\{', '\\(', 'םן', 'ןם', 'יםות', 'ותים'].join('|'); 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests and Linting 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 14.x 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14.x 23 | - run: npm ci 24 | - run: npm test 25 | 26 | lint: 27 | 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js 14.x 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: 14.x 36 | - run: npm ci 37 | - run: npx eslint . -------------------------------------------------------------------------------- /test/rules/beginnings.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE, MALE } from '../../src/ivrita'; 2 | 3 | test('Word beginnings', () => { 4 | expect(genderize('י/תכתוב', FEMALE)).toBe('תכתוב'); 5 | expect(genderize('י/תכתוב', MALE)).toBe('יכתוב'); 6 | expect(genderize('שי/תכתוב', FEMALE)).toBe('שתכתוב'); 7 | expect(genderize('שי/תכתוב', MALE)).toBe('שיכתוב'); 8 | 9 | expect(genderize('ת/יכתוב', FEMALE)).toBe('תכתוב'); 10 | expect(genderize('ת/יכתוב', MALE)).toBe('יכתוב'); 11 | expect(genderize('שת/יכתוב', FEMALE)).toBe('שתכתוב'); 12 | expect(genderize('שת/יכתוב', MALE)).toBe('שיכתוב'); 13 | 14 | expect(genderize('ת/ירצה', FEMALE)).toBe('תרצה'); 15 | expect(genderize('ת/ירצה', MALE)).toBe('ירצה'); 16 | expect(genderize('י/תרצה', FEMALE)).toBe('תרצה'); 17 | expect(genderize('י/תרצה', MALE)).toBe('ירצה'); 18 | 19 | expect(genderize('י/תבוא', FEMALE)).toBe('תבוא'); 20 | expect(genderize('י/תבוא', MALE)).toBe('יבוא'); 21 | expect(genderize('ת/יבוא', FEMALE)).toBe('תבוא'); 22 | expect(genderize('ת/יבוא', MALE)).toBe('יבוא'); 23 | }); 24 | -------------------------------------------------------------------------------- /test/rules/brackets.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | genderize, FEMALE, MALE, NEUTRAL, 3 | } from '../../src/ivrita'; 4 | 5 | test('Two parameters', () => { 6 | expect(genderize('[בן|בת]', FEMALE)).toBe('בת'); 7 | expect(genderize('[בן|בת]', MALE)).toBe('בן'); 8 | 9 | expect(genderize('[נאה|יפה]', FEMALE)).toBe('יפה'); 10 | expect(genderize('[נאה|יפה]', MALE)).toBe('נאה'); 11 | 12 | expect(genderize('[חתיך|מהממת]', FEMALE)).toBe('מהממת'); 13 | expect(genderize('[חתיך|מהממת]', MALE)).toBe('חתיך'); 14 | }); 15 | 16 | test('Two parameters neutral', () => { 17 | expect(genderize('[בן|בת]', NEUTRAL)).toBe('בן/בת'); 18 | }); 19 | 20 | test('Three parameters', () => { 21 | expect(genderize('[בן|בת|ילד]', FEMALE)).toBe('בת'); 22 | expect(genderize('[בן|בת|ילד]', MALE)).toBe('בן'); 23 | expect(genderize('[בן|בת|ילד]', NEUTRAL)).toBe('ילד'); 24 | }); 25 | 26 | test('Partial options', () => { 27 | expect(genderize('תוכל[|י|ו]', FEMALE)).toBe('תוכלי'); 28 | expect(genderize('תוכל[|י|ו]', MALE)).toBe('תוכל'); 29 | expect(genderize('תוכל[|י|ו]', NEUTRAL)).toBe('תוכלו'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/ui/switch.js: -------------------------------------------------------------------------------- 1 | import JSXComponent from 'jsx-render/lib/JSXComponent'; 2 | 3 | export default class IvritaSwitch extends JSXComponent { 4 | /** 5 | * The name of the event to be dispatched on `document` before reading the configuration 6 | */ 7 | static EVENT_INIT = 'ivrita-ui-ready'; 8 | 9 | /** 10 | * @property {Ivrita[]} ivritaInstances 11 | */ 12 | ivritaInstances = []; 13 | 14 | /** 15 | * @param {...Ivrita} ivritaInstances 16 | */ 17 | constructor(...ivritaInstances) { 18 | super(); 19 | if (ivritaInstances.length) { 20 | this.setIvritaInstances(...ivritaInstances); 21 | } 22 | } 23 | 24 | /** 25 | * @param {...Ivrita} ivritaInstances 26 | */ 27 | setIvritaInstances(...ivritaInstances) { 28 | this.ivritaInstances = ivritaInstances; 29 | } 30 | 31 | /** 32 | * Sets the mode for all ivritaInstances of this switch 33 | * @param {string} modeStr The new mode 34 | */ 35 | setMode(mode) { 36 | this.ivritaInstances.forEach((i) => i.setMode(mode)); 37 | window.localStorage.setItem('ivrita-mode', mode); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/textObject.js: -------------------------------------------------------------------------------- 1 | import { genderize, ORIGINAL } from './ivrita'; 2 | 3 | export const IncompatibleTypeError = new Error('Incompatible node passed to the node constructor'); 4 | 5 | export default class TextObject { 6 | originalText = ''; 7 | 8 | currentMode = false; 9 | 10 | storedValues = {} 11 | 12 | static instances = new WeakMap(); 13 | 14 | constructor(node) { 15 | if (TextObject.instances.has(node)) { 16 | return TextObject.instances.get(node); 17 | } 18 | 19 | TextObject.instances.set(node, this); 20 | } 21 | 22 | init() { 23 | this.originalText = this.getValue(); 24 | } 25 | 26 | setMode(newMode) { 27 | let newVal; 28 | this.currentMode = newMode; 29 | 30 | if (this.storedValues[newMode] !== undefined) { 31 | newVal = this.storedValues[newMode]; 32 | } else if (newMode === ORIGINAL) { 33 | newVal = this.originalText; 34 | } else { 35 | newVal = genderize(this.originalText, newMode); 36 | } 37 | 38 | if (newVal !== this.getValue()) { 39 | this.setValue(newVal); 40 | } 41 | } 42 | 43 | getValue() { 44 | return this.value; 45 | } 46 | 47 | setValue(newVal) { 48 | this.value = newVal; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/textElement.js: -------------------------------------------------------------------------------- 1 | import TextObject, { IncompatibleTypeError } from './textObject'; 2 | import { MALE, FEMALE, NEUTRAL } from './ivrita'; 3 | 4 | export const MALE_DATA_ATTR = 'ivritaMale'; 5 | export const FEMALE_DATA_ATTR = 'ivritaFemale'; 6 | export const NEUTRAL_DATA_ATTR = 'ivritaNeutral'; 7 | 8 | export default class TextElement extends TextObject { 9 | element = {}; 10 | 11 | constructor(element) { 12 | if (!element) return false; 13 | if (!(element instanceof Element)) { 14 | throw IncompatibleTypeError; 15 | } 16 | 17 | super(element); 18 | 19 | this.element = element; 20 | 21 | if (!this.currentMode) { 22 | this.init(); 23 | } 24 | } 25 | 26 | init() { 27 | super.init(); 28 | if (this.element.dataset[MALE_DATA_ATTR]) { 29 | this.storedValues[MALE] = this.element.dataset[MALE_DATA_ATTR]; 30 | } 31 | if (this.element.dataset[FEMALE_DATA_ATTR]) { 32 | this.storedValues[FEMALE] = this.element.dataset[FEMALE_DATA_ATTR]; 33 | } 34 | if (this.element.dataset[NEUTRAL_DATA_ATTR]) { 35 | this.storedValues[NEUTRAL] = this.element.dataset[NEUTRAL_DATA_ATTR]; 36 | } 37 | } 38 | 39 | getValue() { 40 | return this.element.innerHTML; 41 | } 42 | 43 | setValue(newVal) { 44 | this.element.innerHTML = newVal; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/utils/finals.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | finnables, toFin, toNotFin, 3 | } from '../../src/utils/finals'; 4 | 5 | test('Finnables helper', () => { 6 | expect(finnables).toContain('ם'); 7 | expect(finnables).toContain('מ'); 8 | expect(finnables).toContain('ן'); 9 | expect(finnables).toContain('ץ'); 10 | expect(finnables).toContain('כ'); 11 | 12 | expect(finnables).not.toContain('ד'); 13 | expect(finnables).not.toContain('5'); 14 | expect(finnables).not.toContain('j'); 15 | }); 16 | 17 | test('toFin helper', () => { 18 | expect(toFin('מ')).toEqual('ם'); 19 | expect(toFin('נ')).toEqual('ן'); 20 | expect(toFin('צ')).toEqual('ץ'); 21 | expect(toFin('כ')).toEqual('ך'); 22 | 23 | expect(toFin('ץ')).toEqual('ץ'); 24 | expect(toFin('ם')).toEqual('ם'); 25 | 26 | expect(toFin('י')).toEqual('י'); 27 | expect(toFin('8')).toEqual('8'); 28 | expect(toFin('h')).toEqual('h'); 29 | }); 30 | 31 | test('toNotFin helper', () => { 32 | expect(toNotFin('ם')).toEqual('מ'); 33 | expect(toNotFin('ן')).toEqual('נ'); 34 | expect(toNotFin('ץ')).toEqual('צ'); 35 | expect(toNotFin('ך')).toEqual('כ'); 36 | 37 | expect(toNotFin('צ')).toEqual('צ'); 38 | expect(toNotFin('מ')).toEqual('מ'); 39 | 40 | expect(toNotFin('י')).toEqual('י'); 41 | expect(toNotFin('8')).toEqual('8'); 42 | expect(toNotFin('h')).toEqual('h'); 43 | }); 44 | -------------------------------------------------------------------------------- /test/rules/braces.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE, MALE } from '../../src/ivrita'; 2 | 3 | test('Basic braces functionality', () => { 4 | expect(genderize('{רוצים/ות}', FEMALE)).toBe('רוצים/ות'); 5 | expect(genderize('{רוצים/ות}', MALE)).toBe('רוצים/ות'); 6 | 7 | expect(genderize('{הרבה מעצבים/ות רוצים/ות ללמוד תכנות}', FEMALE)).toBe('הרבה מעצבים/ות רוצים/ות ללמוד תכנות'); 8 | expect(genderize('{הרבה מעצבים/ות רוצים/ות ללמוד תכנות}', MALE)).toBe('הרבה מעצבים/ות רוצים/ות ללמוד תכנות'); 9 | }); 10 | 11 | test('Braces mixed in a sentence', () => { 12 | expect(genderize('הרבה מעצבים/ות {רוצים/ות} למצוא את עצמם/ן', FEMALE)).toBe('הרבה מעצבות רוצים/ות למצוא את עצמן'); 13 | expect(genderize('הרבה מעצבים/ות {רוצים/ות} למצוא את עצמם/ן', MALE)).toBe('הרבה מעצבים רוצים/ות למצוא את עצמם'); 14 | 15 | expect(genderize('הרבה {מעצבים/ות} רוצים/ות לראות את {עצמם/ן} כאילו הם/ן יצאו ממצריים כגבר/אישה {חזק.ה} ועצמאי.ת', FEMALE)).toBe('הרבה מעצבים/ות רוצות לראות את עצמם/ן כאילו הן יצאו ממצריים כאישה חזק.ה ועצמאית'); 16 | expect(genderize('הרבה {מעצבים/ות} רוצים/ות לראות את {עצמם/ן} כאילו הם/ן יצאו ממצריים כגבר/אישה {חזק.ה} ועצמאי.ת', MALE)).toBe('הרבה מעצבים/ות רוצים לראות את עצמם/ן כאילו הם יצאו ממצריים כגבר חזק.ה ועצמאי'); 17 | }); 18 | 19 | test('Brackets inside braces', () => { 20 | expect(genderize('{[בן|בת]}', FEMALE)).toBe('[בן|בת]'); 21 | expect(genderize('{[בן|בת]}', MALE)).toBe('[בן|בת]'); 22 | }); 23 | -------------------------------------------------------------------------------- /test/otf.test.js: -------------------------------------------------------------------------------- 1 | import Ivrita from '../src/element'; 2 | 3 | test('setFontFeatureSettings on clean element', () => { 4 | const el = document.createElement('p'); 5 | 6 | expect(el.style.fontFeatureSettings).toEqual(''); 7 | 8 | const ivrita = new Ivrita(el); 9 | 10 | ivrita.setFontFeatureSettings(true); 11 | 12 | expect(el.style.fontFeatureSettings).toEqual('"titl"'); 13 | 14 | ivrita.setFontFeatureSettings(false); 15 | 16 | expect(el.style.fontFeatureSettings).toEqual('normal'); 17 | }); 18 | 19 | test('OpenType setting on element with pre-existing settings', () => { 20 | const el = document.createElement('p'); 21 | 22 | el.style.fontFeatureSettings = '"ss01" 1, "tnum"'; 23 | 24 | const ivrita = new Ivrita(el); 25 | 26 | ivrita.setFontFeatureSettings(true); 27 | 28 | expect(el.style.fontFeatureSettings).toEqual('"ss01" 1, "tnum", "titl"'); 29 | 30 | ivrita.setFontFeatureSettings(false); 31 | 32 | expect(el.style.fontFeatureSettings).toEqual('"ss01" 1, "tnum"'); 33 | }); 34 | 35 | test('setFontFeatureSettings on clean element', () => { 36 | const el = document.createElement('p'); 37 | 38 | expect(el.style.fontFeatureSettings).toEqual(''); 39 | 40 | const ivrita = new Ivrita(el); 41 | 42 | ivrita.setMode(Ivrita.MULTI); 43 | 44 | expect(el.style.fontFeatureSettings).toEqual('"titl"'); 45 | 46 | ivrita.setMode(Ivrita.NEUTRAL); 47 | 48 | expect(el.style.fontFeatureSettings).toEqual('normal'); 49 | }); 50 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Deploy to CDN 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js 14.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14.x 21 | - run: npm ci 22 | - run: npm run build --if-present 23 | - run: npm test 24 | 25 | - name: Deploy to S3 26 | uses: jakejarvis/s3-sync-action@master 27 | with: 28 | args: --acl public-read --follow-symlinks --delete 29 | env: 30 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 31 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 32 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 33 | AWS_REGION: 'eu-central-1' 34 | SOURCE_DIR: 'dist' 35 | 36 | - name: Invalidate CloudFront Cache 37 | uses: chetan/invalidate-cloudfront-action@master 38 | env: 39 | DISTRIBUTION: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION }} 40 | PATHS: '/*' 41 | AWS_REGION: 'eu-central-1' 42 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 43 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ivrita", 3 | "version": "1.0.1", 4 | "description": "Ivrita is an open-source set of typographic tools for gender equality in Hebrew", 5 | "main": "src/ivrita.js", 6 | "browser": "src/element.js", 7 | "directories": { 8 | "example": "examples" 9 | }, 10 | "scripts": { 11 | "build": "webpack", 12 | "watch": "webpack --watch", 13 | "test": "jest", 14 | "test-watch": "jest --watch --notify" 15 | }, 16 | "jest": { 17 | "setupFiles": [ 18 | "./test/setup-jest.js" 19 | ] 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/AlefAlefAlef/ivrita.git" 24 | }, 25 | "author": "Reuven Karasik and Avraham Cornfeld", 26 | "license": "AGPL-3.0-or-later", 27 | "bugs": { 28 | "url": "https://github.com/AlefAlefAlef/ivrita/issues" 29 | }, 30 | "homepage": "https://github.com/AlefAlefAlef/ivrita#readme", 31 | "dependencies": { 32 | "jsx-render": "^1.1.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.12.3", 36 | "@babel/plugin-proposal-class-properties": "^7.12.1", 37 | "@babel/plugin-syntax-jsx": "^7.12.1", 38 | "@babel/plugin-transform-react-jsx": "^7.12.12", 39 | "@babel/preset-env": "^7.12.1", 40 | "babel-cli": "^6.26.0", 41 | "babel-eslint": "^10.1.0", 42 | "babel-loader": "^8.1.0", 43 | "css-loader": "^5.0.1", 44 | "eslint": "^7.12.1", 45 | "eslint-config-airbnb-base": "^14.2.0", 46 | "eslint-plugin-import": "^2.22.1", 47 | "eslint-plugin-jest": "^24.1.0", 48 | "file-loader": "^6.2.0", 49 | "jest": "^26.6.1", 50 | "jest-cli": "^26.6.1", 51 | "jquery": "^3.5.1", 52 | "regenerator-runtime": "^0.11.1", 53 | "sass": "^1.32.5", 54 | "sass-loader": "^10.1.1", 55 | "style-loader": "^2.0.0", 56 | "webpack": "^5.3.2", 57 | "webpack-cli": "^4.1.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/ui/accessibility.test.js: -------------------------------------------------------------------------------- 1 | import defaultSwitch, { initDefaultSwitch } from '../../src/ui/index'; 2 | import { FEMALE } from '../../src/ivrita'; 3 | 4 | // Jest can't read SCSS, and we don't need it anyway 5 | jest.mock('../../src/ui/style.scss', () => ({})); 6 | 7 | beforeAll(() => { 8 | if (!document.getElementsByClassName('ivrita-switch').length) { 9 | initDefaultSwitch(); 10 | } 11 | expect(document.getElementsByClassName('ivrita-switch').length).toEqual(1); 12 | }); 13 | 14 | test('All buttons in default switch contain aria-labels', () => { 15 | expect(Array.from(document.getElementsByClassName('ivrita-button')).map((el) => el.getAttribute('aria-label'))).toStrictEqual([ 16 | 'שינוי לשון הפנייה של האתר לאיש', 17 | 'שינוי לשון הפנייה של האתר לאישה', 18 | 'שינוי לשון הפנייה של האתר לניטרלי', 19 | ]); 20 | }); 21 | 22 | test('The selected button has an aria-selected attribute', () => { 23 | expect(Array.from(document.getElementsByClassName('ivrita-button')).filter((el) => el.getAttribute('aria-selected') === 'true').length).toEqual(1); 24 | 25 | defaultSwitch.setMode(FEMALE); 26 | 27 | expect(Array.from(document.getElementsByClassName('ivrita-button')).filter((el) => el.getAttribute('aria-selected') === 'true').length).toEqual(1); 28 | expect(document.querySelector('.ivrita-mode-changer[data-ivrita-mode="2"]').getAttribute('aria-selected')).toEqual('true'); 29 | }); 30 | 31 | test('The list has the right roles and aria-activedescendant', () => { 32 | defaultSwitch.setMode(FEMALE); 33 | 34 | const defaultSwitchElement = document.getElementById('ivrita-default-switch'); 35 | 36 | expect(defaultSwitchElement.getAttribute('role')).toEqual('listbox'); 37 | expect(defaultSwitchElement.getAttribute('aria-activedescendant')).toEqual(`ivrita-default-switch-button-${FEMALE}`); 38 | 39 | expect( 40 | Array.from(defaultSwitchElement.getElementsByClassName('ivrita-mode-changer')) 41 | .every((e) => e.getAttribute('role') === 'option'), 42 | ).toEqual(true); 43 | }); 44 | -------------------------------------------------------------------------------- /src/ivrita.js: -------------------------------------------------------------------------------- 1 | import rules from './rules'; 2 | 3 | const PROTECTED = '__IVRITA_PROTECTED__'; 4 | const protectedRegexp = new RegExp(`\\{${PROTECTED}:(\\d+):${PROTECTED}\\}`, 'g'); 5 | 6 | export const ORIGINAL = 0; 7 | 8 | export const MALE = 1; 9 | 10 | export const FEMALE = 2; 11 | 12 | export const NEUTRAL = 3; 13 | 14 | export const GENDERS = [ORIGINAL, MALE, FEMALE, NEUTRAL]; 15 | 16 | export const genderize = (originalText, gender, doneFunc) => { 17 | let genderized = originalText; 18 | const bracedStrings = []; 19 | 20 | if (genderized.includes('{')) { 21 | // Remove braced parts from text and save them aside 22 | genderized = genderized.replace(/\{(.*?)\}/g, (matched, string, index) => { 23 | bracedStrings[index] = string; 24 | return `{${PROTECTED}:${index}:${PROTECTED}}`; 25 | }); 26 | } 27 | 28 | let prev = originalText; 29 | const used = []; 30 | rules.forEach(([pattern, male, female, neutral]) => { 31 | let replacement; 32 | switch (gender) { 33 | case FEMALE: 34 | replacement = female; 35 | break; 36 | 37 | case MALE: 38 | replacement = male; 39 | break; 40 | 41 | case NEUTRAL: 42 | default: 43 | if (typeof neutral !== 'undefined') replacement = neutral; 44 | break; 45 | } 46 | if (replacement !== undefined) { 47 | genderized = genderized.replace(pattern, replacement); 48 | } 49 | 50 | if (typeof doneFunc === 'function' && prev !== genderized) { 51 | used.push(pattern); 52 | prev = genderized; 53 | } 54 | }); 55 | 56 | if (bracedStrings.length) { 57 | // Bring back braced parts 58 | genderized = genderized.replace(protectedRegexp, (matched, group) => { 59 | const parsedIndex = parseInt(group, 10); 60 | if (bracedStrings[parsedIndex]) { 61 | return bracedStrings[parsedIndex]; 62 | } 63 | return ''; 64 | }); 65 | } 66 | 67 | if (typeof doneFunc === 'function') { 68 | doneFunc(used); 69 | } 70 | return genderized; 71 | }; 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // babel loader, testing for files that have a .js(x) extension 2 | // (except for files in our node_modules folder!). 3 | const babelLoader = { 4 | test: /\.jsx?$/, 5 | exclude: /node_modules/, 6 | loader: 'babel-loader', 7 | options: { 8 | compact: true, // because I want readable output 9 | }, 10 | }; 11 | 12 | module.exports = [{ 13 | // entry is the "main" source file we want to include/import 14 | entry: './src/element.js', 15 | devtool: 'source-map', 16 | // output tells webpack where to put the bundle it creates 17 | output: { 18 | // in the case of a "plain global browser library", this 19 | // will be used as the reference to our module that is 20 | // hung off of the window object. 21 | library: 'Ivrita', 22 | // We want webpack to build a UMD wrapper for our module 23 | libraryTarget: 'umd', 24 | // the destination file name 25 | filename: 'ivrita.min.js', 26 | libraryExport: 'default', 27 | }, 28 | module: { 29 | rules: [ 30 | babelLoader, 31 | ], 32 | }, 33 | }, { 34 | entry: './src/ui/index.js', 35 | devtool: 'source-map', 36 | output: { 37 | library: ['Ivrita', 'ui'], 38 | libraryTarget: 'umd', 39 | filename: 'ivrita.ui.min.js', 40 | libraryExport: 'default', 41 | }, 42 | externals: { 43 | '../element': 'Ivrita', 44 | }, 45 | module: { 46 | rules: [ 47 | babelLoader, 48 | { 49 | test: /\.s[ac]ss$/i, 50 | use: [ 51 | // Creates `style` nodes from JS strings 52 | 'style-loader', 53 | // Translates CSS into CommonJS 54 | 'css-loader', 55 | // Compiles Sass to CSS 56 | 'sass-loader', 57 | ], 58 | }, 59 | { // Keep font files the same, in the dist/fonts directory 60 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 61 | use: [ 62 | { 63 | loader: 'file-loader', 64 | options: { 65 | name: '[name].[ext]', 66 | outputPath: 'fonts/', 67 | }, 68 | }, 69 | ], 70 | }, 71 | ], 72 | }, 73 | }]; 74 | -------------------------------------------------------------------------------- /test/attributes.test.js: -------------------------------------------------------------------------------- 1 | import Ivrita from '../src/element'; 2 | 3 | import { FEMALE } from '../src/ivrita'; 4 | 5 | const template = ` 6 |
7 |
8 |

הרשמ/י עכשיו!

9 | 10 | 11 | התנתק/י 12 | 13 | 14 | 15 |
16 |
17 | `; 18 | 19 | test('Input placeholder', () => { 20 | document.body.innerHTML = template; 21 | const name = document.getElementById('name'); 22 | const city = document.getElementById('city'); 23 | 24 | const ivrita = new Ivrita(document.body); 25 | ivrita.setMode(FEMALE); 26 | 27 | expect(name.getAttribute('placeholder')).toBe('הכניסי את שמך כאן'); 28 | expect(city.getAttribute('placeholder')).toBe('הכניסי את העיר שבה את גרה כאן'); 29 | }); 30 | 31 | test('Link title', () => { 32 | document.body.innerHTML = template; 33 | const link = document.querySelector('a'); 34 | 35 | const ivrita = new Ivrita(document.body); 36 | ivrita.setMode(FEMALE); 37 | 38 | expect(link.innerHTML).toBe('התנתקי'); 39 | expect(link.getAttribute('title')).toBe('לחצי כאן כדי להתנתק'); 40 | }); 41 | 42 | test('Buttons', () => { 43 | document.body.innerHTML = template; 44 | const button = document.querySelector('button'); 45 | const inputSubmit = document.querySelector('input[type=submit]'); 46 | const inputButton = document.querySelector('input[type=button]'); 47 | 48 | const ivrita = new Ivrita(document.body); 49 | ivrita.setMode(FEMALE); 50 | 51 | expect(button.innerHTML).toBe('נקי את הטופס'); 52 | expect(inputSubmit.value).toBe('שלחי'); 53 | expect(inputButton.value).toBe('בטלי'); 54 | }); 55 | 56 | test('Custom attribute', () => { 57 | document.body.innerHTML = template; 58 | const button = document.querySelector('button'); 59 | 60 | const ivrita = new Ivrita(document.body); 61 | ivrita.relavantAttributes['button[data-custom-text]'] = ['data-custom-text']; 62 | ivrita.registerTextAttributes(document.body); 63 | ivrita.setMode(FEMALE); 64 | 65 | expect(button.getAttribute('data-custom-text')).toBe('לא יודעת איך?'); 66 | }); 67 | -------------------------------------------------------------------------------- /src/ui/custom.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import dom from 'jsx-render'; 3 | import IvritaElement from '../element'; 4 | import { MALE, FEMALE, NEUTRAL } from '../ivrita'; 5 | import IvritaSwitch from './switch'; 6 | 7 | export default class CustomSwitch extends IvritaSwitch { 8 | buttons = new Map(); 9 | 10 | constructor(element, buttonsSelector, ...ivritaInstances) { 11 | if (ivritaInstances.length === 0 && typeof window._ivrita !== 'undefined') { 12 | super(window._ivrita); 13 | } else { 14 | super(...ivritaInstances); 15 | } 16 | 17 | this.element = element; 18 | this.buttonsSelector = buttonsSelector; 19 | 20 | this.listenForClicks(); 21 | this.listenForExternalChanges(); 22 | } 23 | 24 | listenForClicks() { 25 | Array.from(this.element.querySelectorAll(this.buttonsSelector)).forEach((button) => { 26 | const btnModeStr = button.dataset.ivritaMode; 27 | let btnMode; 28 | switch (btnModeStr.toUpperCase()) { 29 | case 'MALE': 30 | btnMode = MALE; 31 | break; 32 | 33 | case 'FEMALE': 34 | btnMode = FEMALE; 35 | break; 36 | 37 | case 'NEUTRAL': 38 | btnMode = NEUTRAL; 39 | break; 40 | 41 | default: 42 | btnMode = null; 43 | } 44 | 45 | if (btnMode !== null) { 46 | button.addEventListener('click', (e) => { 47 | e.preventDefault(); 48 | 49 | this.setMode(btnMode); 50 | this.setActiveButton(btnMode); 51 | }); 52 | 53 | this.buttons.set(button, btnMode); 54 | } 55 | }); 56 | } 57 | 58 | listenForExternalChanges() { 59 | this.ivritaInstances.forEach((ivritaInstance) => { 60 | if (ivritaInstance.elements.length) { 61 | ivritaInstance.elements[0].addEventListener(IvritaElement.EVENT_MODE_CHANGED, 62 | ({ detail: { mode, firingInstance } }) => { 63 | if (firingInstance === ivritaInstance) { // Skip events bubbled by other instances 64 | this.setActiveButton(mode); 65 | } 66 | }); 67 | } 68 | }); 69 | } 70 | 71 | setActiveButton(newMode) { 72 | this.buttons.forEach((value, btn) => { 73 | if (newMode === value) { 74 | btn.classList.add('ivrita-active'); 75 | } else { 76 | btn.classList.remove('ivrita-active'); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/rules/parenthesis.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | genderize, FEMALE, MALE, NEUTRAL, 3 | } from '../../src/ivrita'; 4 | 5 | test('Singular possessive', () => { 6 | expect(genderize('חבר(ת)ו', FEMALE)).toBe('חברתו'); 7 | expect(genderize('חבר(ת)ו', MALE)).toBe('חברו'); 8 | }); 9 | 10 | test('Grandma/pa', () => { 11 | expect(genderize('סב(ת)א', FEMALE)).toBe('סבתא'); 12 | expect(genderize('סב(ת)א', MALE)).toBe('סבא'); 13 | }); 14 | 15 | test('Men/Women', () => { 16 | expect(genderize('(א)נשים', FEMALE)).toBe('נשים'); 17 | expect(genderize('(א)נשים', MALE)).toBe('אנשים'); 18 | }); 19 | 20 | test('Third person ending Tav', () => { 21 | expect(genderize('חושב(ת)', FEMALE)).toBe('חושבת'); 22 | expect(genderize('חושב(ת)', MALE)).toBe('חושב'); 23 | }); 24 | 25 | test('Plural possessive', () => { 26 | expect(genderize('מתנגד(ות)יו', FEMALE)).toBe('מתנגדותיו'); 27 | expect(genderize('מתנגד(ות)יו', MALE)).toBe('מתנגדיו'); 28 | 29 | expect(genderize('מתנגד(ות)יהן', FEMALE)).toBe('מתנגדותיהן'); 30 | expect(genderize('מתנגד(ות)יהן', MALE)).toBe('מתנגדיהן'); 31 | 32 | expect(genderize('מעריצ(ות)יו', FEMALE)).toBe('מעריצותיו'); 33 | expect(genderize('מעריצ(ות)יו', MALE)).toBe('מעריציו'); 34 | }); 35 | 36 | test('Yod in the middle', () => { 37 | expect(genderize('עלי(י)ך', FEMALE)).toBe('עלייך'); 38 | expect(genderize('עלי(י)ך', MALE)).toBe('עליך'); 39 | 40 | expect(genderize('פנ(י)יך', FEMALE)).toBe('פנייך'); 41 | expect(genderize('פנ(י)יך', MALE)).toBe('פניך'); 42 | 43 | expect(genderize('הפע(י)ל/י', FEMALE)).toBe('הפעילי'); 44 | expect(genderize('הפע(י)ל/י', MALE)).toBe('הפעל'); 45 | }); 46 | 47 | test('Neutral plural', () => { 48 | expect(genderize('הודעתך(ם)', FEMALE)).toBe('הודעתך'); 49 | expect(genderize('הודעתך(ם)', MALE)).toBe('הודעתך'); 50 | expect(genderize('הודעתך(ם)', NEUTRAL)).toBe('הודעתכם'); 51 | expect(genderize('הודעתך(ן)', NEUTRAL)).toBe('הודעתכן'); 52 | expect(genderize('הודעתך(םן)', NEUTRAL)).toBe('הודעתכםן'); 53 | expect(genderize('הודעתך(ןם)', NEUTRAL)).toBe('הודעתכןם'); 54 | expect(genderize('הודעתך(ם.ן)', NEUTRAL)).toBe('הודעתכם.ן'); 55 | expect(genderize('הודעתך(ם/ן)', NEUTRAL)).toBe('הודעתכם/ן'); 56 | 57 | expect(genderize('שלך(ם)', FEMALE)).toBe('שלך'); 58 | expect(genderize('שלך(ם)', MALE)).toBe('שלך'); 59 | expect(genderize('שלך(ם)', NEUTRAL)).toBe('שלכם'); 60 | expect(genderize('שלך(ן)', NEUTRAL)).toBe('שלכן'); 61 | 62 | expect(genderize('שלכ(ם)', FEMALE)).toBe('שלך'); 63 | expect(genderize('שלכ(ם)', MALE)).toBe('שלך'); 64 | expect(genderize('שלכ(ם)', NEUTRAL)).toBe('שלכם'); 65 | expect(genderize('שלכ(ן)', NEUTRAL)).toBe('שלכן'); 66 | }); 67 | -------------------------------------------------------------------------------- /test/rules/predefined.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE, MALE } from '../../src/ivrita'; 2 | 3 | test('Whole words', () => { 4 | expect(genderize('אנשי/ות עסקים', FEMALE)).toBe('נשות עסקים'); 5 | expect(genderize('אנשי/ות עסקים', MALE)).toBe('אנשי עסקים'); 6 | 7 | expect(genderize('א.נשים', FEMALE)).toBe('נשים'); 8 | expect(genderize('א.נשים', MALE)).toBe('אנשים'); 9 | 10 | expect(genderize('א.נשי', FEMALE)).toBe('נשות'); 11 | expect(genderize('א.נשי', MALE)).toBe('אנשי'); 12 | 13 | expect(genderize('איש/ת', FEMALE)).toBe('אשת'); 14 | expect(genderize('איש/ת', MALE)).toBe('איש'); 15 | expect(genderize('איש/אשת', FEMALE)).toBe('אשת'); 16 | expect(genderize('איש/אשת', MALE)).toBe('איש'); 17 | expect(genderize('אשת/איש', FEMALE)).toBe('אשת'); 18 | expect(genderize('אשת/איש', MALE)).toBe('איש'); 19 | expect(genderize('גבר/אישה', FEMALE)).toBe('אישה'); 20 | expect(genderize('גבר/אישה', MALE)).toBe('גבר'); 21 | expect(genderize('איש/אישה', MALE)).toBe('איש'); 22 | 23 | expect(genderize('אח/ות', FEMALE)).toBe('אחות'); 24 | expect(genderize('אח/ות', MALE)).toBe('אח'); 25 | 26 | expect(genderize('אחי/ותי', FEMALE)).toBe('אחותי'); 27 | expect(genderize('אחי/ותי', MALE)).toBe('אחי'); 28 | 29 | expect(genderize('לו/לה, לה/לו', FEMALE)).toBe('לה, לה'); 30 | expect(genderize('לו/לה, לה/לו', MALE)).toBe('לו, לו'); 31 | 32 | expect(genderize('בן/בת, בת/בן', FEMALE)).toBe('בת, בת'); 33 | expect(genderize('בן/בת, בת/בן', MALE)).toBe('בן, בן'); 34 | 35 | expect(genderize('הוא/היא, היא/הוא', FEMALE)).toBe('היא, היא'); 36 | expect(genderize('הוא/היא, היא/הוא', MALE)).toBe('הוא, הוא'); 37 | 38 | expect(genderize('אנשי/ות', FEMALE)).toBe('נשות'); 39 | expect(genderize('אנשי/ות', MALE)).toBe('אנשי'); 40 | expect(genderize('א.נשות', FEMALE)).toBe('נשות'); 41 | expect(genderize('א.נשות', MALE)).toBe('אנשי'); 42 | 43 | expect(genderize('מישהו/י', FEMALE)).toBe('מישהי'); 44 | expect(genderize('מישהו/י', MALE)).toBe('מישהו'); 45 | 46 | expect(genderize('אחד/ת, אחד/אחת, אחת/אחד', FEMALE)).toBe('אחת, אחת, אחת'); 47 | expect(genderize('אחד/ת, אחד/אחת, אחת/אחד', MALE)).toBe('אחד, אחד, אחד'); 48 | 49 | expect(genderize('יקירי/תי', FEMALE)).toBe('יקירתי'); 50 | expect(genderize('יקירי/תי', MALE)).toBe('יקירי'); 51 | 52 | expect(genderize('זה/זאת', FEMALE)).toBe('זאת'); 53 | expect(genderize('זה/זאת', MALE)).toBe('זה'); 54 | expect(genderize('זה/ו', FEMALE)).toBe('זו'); 55 | expect(genderize('זה/ו', MALE)).toBe('זה'); 56 | expect(genderize('זו/ה', FEMALE)).toBe('זו'); 57 | expect(genderize('זו/ה', MALE)).toBe('זה'); 58 | 59 | expect(genderize('נשוי/אה', FEMALE)).toBe('נשואה'); 60 | expect(genderize('נשוי/אה', MALE)).toBe('נשוי'); 61 | expect(genderize('נשוי/ה', FEMALE)).toBe('נשואה'); 62 | expect(genderize('נשוי/ה', MALE)).toBe('נשוי'); 63 | }); 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![npm](https://img.shields.io/npm/v/ivrita?label=npm%20version) 2 | ![Tests and Linting](https://github.com/AlefAlefAlef/ivrita/workflows/Tests%20and%20Linting/badge.svg) 3 | 4 | 5 |

6 | 7 | Ivrita Logo 8 | 9 | 10 |

Ivrita JavaScript Library

11 | 12 |

13 | Ivrita is an open-source set of typographic tools for gender equality in Hebrew. 14 |
15 | Read about the project in Hebrew » 16 |
17 |
18 | Chrome Extension 19 | · 20 | WordPress Plugin 21 |

22 |

23 | 24 | ## Install on your website 25 | > If your website uses WordPress, check out the [WordPress Plugin](https://he.wordpress.org/plugins/ivrita) for easy usage 26 | 27 | Installing the library on your website requires 3 steps: 28 | 1. Include the minified JS file: 29 | ```html 30 | 31 | ``` 32 | 2. Initialize the Ivrita object: 33 | * First argument is the DOM element(s) for which texts should be changed (default: `` and ``) 34 | * Second argument is the initial gender to be set (default: `Ivrita.NEUTRAL`) 35 | ```JavaScript 36 | var ivrita = new Ivrita(document.querySelector('#content'), Ivrita.FEMALE); 37 | ``` 38 | 39 | 3. Change the gender later with `setMode` on the object instance: 40 | ```JavaScript 41 | ivrita.setMode(Ivrita.MALE); // Possible options: Ivrita.MALE, Ivrita.FEMALE, Ivrita.NEUTRAL, Ivrita.ORIGINAL 42 | ``` 43 | That's it! 44 | 45 | ## Install Source 46 | 47 | Use `npm` to install the package files locally, to include in another JS library: 48 | 49 | ```bash 50 | npm install ivrita 51 | ``` 52 | 53 | ```JavaScript 54 | import Ivrita from 'ivrita'; 55 | 56 | console.log(Ivrita.genderize('ברוכים/ות הבאות/ים', Ivrita.FEMALE)); 57 | ``` 58 | 59 | ## Contributing 60 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 61 | 62 | Most typical changes to the Hebrew genderization should be added to the `rules.js` file. Note that the order of rules matters, since the rules are executed one by one and can conflict. 63 | 64 | Use `npm test` (or `npm run test-watch`) to make sure your changes didn't break any functionality, and please make sure to update tests for new features or bug fixes. 65 | -------------------------------------------------------------------------------- /test/rules/unchanged.test.js: -------------------------------------------------------------------------------- 1 | import { genderize, FEMALE, MALE } from '../../src/ivrita'; 2 | 3 | test('Multiple options slash', () => { 4 | expect(genderize('ו/או', FEMALE)).toBe('ו/או'); 5 | expect(genderize('ו/או', MALE)).toBe('ו/או'); 6 | 7 | expect(genderize('או/או', FEMALE)).toBe('או/או'); 8 | expect(genderize('או/או', MALE)).toBe('או/או'); 9 | }); 10 | 11 | test('Multiple words slash', () => { 12 | expect(genderize('אבן/בתיאבון', FEMALE)).toBe('אבן/בתיאבון'); 13 | expect(genderize('אבן/בתיאבון', MALE)).toBe('אבן/בתיאבון'); 14 | 15 | expect(genderize('יספרו/נהרו', FEMALE)).toBe('יספרו/נהרו'); // unchanged 16 | expect(genderize('יספרו/נהרו', MALE)).toBe('יספרו/נהרו'); // unchanged 17 | 18 | expect(genderize('הלו/להבה', FEMALE)).toBe('הלו/להבה'); // unchanged 19 | expect(genderize('הלו/להבה', MALE)).toBe('הלו/להבה'); // unchanged 20 | 21 | expect(genderize('בכה/תהום/שלום', FEMALE)).toBe('בכה/תהום/שלום'); // unchanged 22 | expect(genderize('בכה/תהום/שלום', MALE)).toBe('בכה/תהום/שלום'); // unchanged 23 | 24 | expect(genderize('יזם/יתד', FEMALE)).toBe('יזם/יתד'); // unchanged 25 | expect(genderize('יזם/יתד', MALE)).toBe('יזם/יתד'); // unchanged 26 | 27 | expect(genderize('יזמים/יותר', FEMALE)).toBe('יזמים/יותר'); // unchanged 28 | expect(genderize('יזמים/יותר', MALE)).toBe('יזמים/יותר'); // unchanged 29 | 30 | expect(genderize('ים/הר', FEMALE)).toBe('ים/הר'); // unchanged 31 | expect(genderize('ים/הר', MALE)).toBe('ים/הר'); // unchanged 32 | }); 33 | 34 | test('Singular/Plural slash', () => { 35 | expect(genderize('תפקיד/ים', FEMALE)).toBe('תפקיד/ים'); // unchanged 36 | expect(genderize('תפקיד/ים', MALE)).toBe('תפקיד/ים'); // unchanged 37 | }); 38 | 39 | test('Final letters and abbreviations', () => { 40 | expect(genderize('דרושים׳', FEMALE)).toBe('דרושים׳'); // unchanged 41 | expect(genderize('דרושים׳', MALE)).toBe('דרושים׳'); // unchanged 42 | 43 | expect(genderize('מע”מ', FEMALE)).toBe('מע”מ'); // unchanged 44 | expect(genderize('מע”מ', MALE)).toBe('מע”מ'); // unchanged 45 | 46 | expect(genderize('פ', FEMALE)).toBe('פ'); // unchanged 47 | expect(genderize('פ', MALE)).toBe('פ'); // unchanged 48 | 49 | expect(genderize('דסקטופ', FEMALE)).toBe('דסקטופ'); // unchanged 50 | expect(genderize('דסקטופ', MALE)).toBe('דסקטופ'); // unchanged 51 | }); 52 | 53 | test('Words which look like genders', () => { 54 | expect(genderize('תותים', FEMALE)).toBe('תותים'); // unchanged 55 | expect(genderize('תותים', MALE)).toBe('תותים'); // unchanged 56 | 57 | expect(genderize('צוותים', FEMALE)).toBe('צוותים'); // unchanged 58 | expect(genderize('צוותים', MALE)).toBe('צוותים'); // unchanged 59 | }); 60 | 61 | test('Words that look like beginnings', () => { 62 | expect(genderize('מישהי/תבוא', FEMALE)).toBe('מישהי/תבוא'); // unchanged 63 | expect(genderize('מישהי/תבוא', MALE)).toBe('מישהי/תבוא'); // unchanged 64 | }); 65 | -------------------------------------------------------------------------------- /src/ui/default.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import dom from 'jsx-render'; 3 | import { 4 | iconTitle, 5 | aboutLinkText, 6 | defaultMaleLabel, 7 | defaultFemaleLabel, 8 | defaultNeutralLabel, 9 | ariaLabel, 10 | } from './hebrew'; 11 | 12 | import './style.scss'; 13 | 14 | import Ivrita from '../element'; 15 | import IvritaSwitch from './switch'; 16 | 17 | export default class DefaultSwitch extends IvritaSwitch { 18 | /** 19 | * The switch configuration. 20 | * 21 | * The defaults are listed bellow, properties can be overridden by using 22 | * the `ivrita-ui-ready` event: 23 | * 24 | * ``` 25 | * document.addEventListener('ivrita-ui-ready', function() { 26 | * Ivrita.ui.default.config.modes[Ivrita.MALE].label = 'גבר'; 27 | * }); 28 | * ``` 29 | */ 30 | config = { 31 | position: 'left', 32 | iconTitle, 33 | aboutLinkText, 34 | ariaLabel, 35 | aboutLinkURL: 'https://alefalefalef.co.il/ivrita/', 36 | style: 0, 37 | logoIcon: '⚥︎', 38 | modes: { 39 | [Ivrita.MALE]: { 40 | label: defaultMaleLabel, 41 | icon: '♂︎', 42 | order: 1, 43 | }, 44 | [Ivrita.FEMALE]: { 45 | label: defaultFemaleLabel, 46 | icon: '♀︎', 47 | order: 2, 48 | }, 49 | [Ivrita.NEUTRAL]: { 50 | label: defaultNeutralLabel, 51 | icon: '⚥︎', 52 | order: 3, 53 | }, 54 | }, 55 | default: Ivrita.NEUTRAL, 56 | } 57 | 58 | /** 59 | * @type {HTMLElement} 60 | */ 61 | element; 62 | 63 | setMode(mode) { 64 | this.element.querySelectorAll('a.ivrita-mode-changer').forEach((e) => { 65 | if (parseInt(e.dataset.ivritaMode, 10) === mode) { 66 | e.classList.add('ivrita-active'); 67 | e.setAttribute('aria-selected', 'true'); 68 | document.getElementById('ivrita-default-switch').setAttribute('aria-activedescendant', e.getAttribute('id')); 69 | } else { 70 | e.classList.remove('ivrita-active'); 71 | e.setAttribute('aria-selected', 'false'); 72 | } 73 | }); 74 | this.element.querySelector(`a[data-ivrita-mode="${mode}"]`).classList.add('ivrita-active'); 75 | 76 | super.setMode(mode); 77 | } 78 | 79 | render() { 80 | return ( 81 | <div id="ivrita-default-switch" class={`ivrita-switch ivrita-switch--${this.config.position}`} tabindex="0" title={ this.config.iconTitle } role="listbox"> 82 | <a href="#" class="ivrita-logo" tabindex="-1" title={ this.config.iconTitle } dangerouslySetInnerHTML={{ __html: this.config.logoIcon }}></a> 83 | { 84 | Object.keys(this.config.modes) 85 | .sort((mode1, mode2) => this.config.modes[mode1].order - this.config.modes[mode2].order) 86 | .map((mode) => parseInt(mode, 10)) 87 | .map((mode) => ( 88 | <a href="#" 89 | id={`ivrita-default-switch-button-${mode}`} 90 | class={`ivrita-mode-changer ivrita-button ivrita-button-style-${this.config.style}`} 91 | data-ivrita-mode={ mode } 92 | title={ this.config.modes[mode].label } 93 | aria-label={ this.config.ariaLabel && this.config.ariaLabel.includes('%s') 94 | ? this.config.ariaLabel.replace('%s', this.config.modes[mode].label) 95 | : this.config.modes[mode].label } 96 | role="option" 97 | ref={ super.ref } 98 | onClick={ (e) => { e.preventDefault(); this.setMode(mode); } } 99 | dangerouslySetInnerHTML={{ __html: this.config.modes[mode].icon }} 100 | ></a> 101 | )) 102 | } 103 | <a href={ this.config.aboutLinkURL } class="ivrita-info-link" title={ this.config.aboutLinkText } target="_blank">ⓘ</a> 104 | </div> 105 | ); 106 | } 107 | 108 | build() { 109 | this.element = this.render(); 110 | } 111 | 112 | init() { 113 | // Dispatch the event, in order to allow external reconfiguration 114 | document.dispatchEvent(new CustomEvent(this.constructor.EVENT_INIT, { bubbles: true })); 115 | 116 | this.build(); 117 | document.body.appendChild(this.element); 118 | 119 | let storedMode = window.localStorage.getItem('ivrita-mode'); 120 | if (!Number.isNaN(parseInt(storedMode, 10))) { 121 | storedMode = parseInt(storedMode, 10); 122 | } 123 | if (!Ivrita.GENDERS.includes(storedMode)) { 124 | if (Ivrita.GENDERS.includes(Ivrita[storedMode])) { 125 | storedMode = Ivrita[storedMode]; 126 | } else { 127 | storedMode = Ivrita.defaultMode; 128 | } 129 | } 130 | if (storedMode) { 131 | this.setMode(storedMode); 132 | } else if (this.config.default) { 133 | this.setMode(this.config.default); 134 | } 135 | } 136 | 137 | rebuild() { 138 | this.element.remove(); 139 | this.init(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/rules.js: -------------------------------------------------------------------------------- 1 | import { 2 | SEP, G, W, FIN, B, HEB, NCB, CB, 3 | } from './utils/characters'; 4 | 5 | import { 6 | finals, 7 | finnables, toFin, toNotFin, 8 | } from './utils/finals'; 9 | import { 10 | custom, verbsFemaleExtraYod, verbsFemaleKeepVav, pluralsWithExtraYod, 11 | } from './wordlists'; 12 | 13 | // Marks are used by early rules to specify a position in a text 14 | // which should be addressed later by later rules. 15 | // For example, M_WORDFIN marks an artificial end of a word, 16 | // which was created by a rule (i.e. wasn't the end of the word in the original text) 17 | // and this ending should be checked for final letters errors (מ=>ם). 18 | const M_WORDFIN = '\u05c8'; // Not a real character 19 | const M_NOT_WORDFIN = '\u05c9'; // Not a real character 20 | 21 | const regexize = (p) => { 22 | p[0] = new RegExp(p[0], 'g'); 23 | 24 | if (p[3] === true) { 25 | p[3] = `${p[1]}/${p[2]}`; 26 | } 27 | return p; 28 | }; 29 | 30 | // Unisex rules match always replace the same string, mode-ignorantly. 31 | const unisex = (pattern, replacement) => [pattern, replacement, replacement, replacement]; 32 | 33 | const matchAndNormalizeVerb = (word, addYod) => { 34 | const wordWithoutLastLetter = word.slice(0, word.length - 1); 35 | const lastLetter = word.slice(word.length - 1); 36 | let lastLetterMatcher = `(${lastLetter})`; 37 | if (finnables.includes(lastLetter)) { 38 | lastLetterMatcher = `(${toFin(lastLetter)}|${toNotFin(lastLetter)})`; 39 | } 40 | 41 | const femaleBase = `${wordWithoutLastLetter}${addYod ? 'י' : ''}${toNotFin(lastLetter)}`; 42 | const male = `${wordWithoutLastLetter}${toFin(lastLetter)}`; 43 | return [ 44 | [`${wordWithoutLastLetter}${lastLetterMatcher}${SEP}י${SEP}ו${B}`, male, `${femaleBase}י`, `${femaleBase}ו`], 45 | [`${wordWithoutLastLetter}${lastLetterMatcher}${SEP}י${B}`, male, `${femaleBase}י`], 46 | ]; 47 | }; 48 | 49 | export default [ 50 | // Whole Words 51 | ...custom, 52 | 53 | // הקשב/י => הקשב, הקשיבי 54 | ...verbsFemaleExtraYod.reduce((r, word) => r.concat(matchAndNormalizeVerb(word, true)), []), 55 | 56 | // קום/י => קום, קומי 57 | ...verbsFemaleKeepVav.reduce((r, word) => r.concat(matchAndNormalizeVerb(word, false)), []), 58 | 59 | // סטודנטים/ות => סטודנטים, סטודנטיות 60 | ...pluralsWithExtraYod.map((word) => { 61 | let targetWord = word; 62 | if (word.includes('(')) { // regex groups 63 | targetWord = word.replace(new RegExp('\\(.*?\\)'), '$2'); // TODO: support multiple groups 64 | } 65 | return [`${CB}${word}ים${SEP}י?ות${B}`, `$1${targetWord}ים`, `$1${targetWord}יות`]; 66 | }), 67 | 68 | // Beginnings 69 | [`(${NCB}${W}{0,3})י${SEP}ת(${W}{2,})`, '$1י$2', '$1ת$2'], // שי/תכתוב 70 | [`(${NCB}${W}{0,3})ת${SEP}י(${W}{2,})`, '$1י$2', '$1ת$2'], // שת/יכתוב 71 | 72 | // Endings 73 | 74 | [`ו${SEP}ה${B}`, 'ו', 'ה'], // בגללה/ו 75 | [`ה${SEP}ו${B}`, 'ו', 'ה'], // בגללו/ה 76 | [`(${W})${SEP}ה${B}`, `$1${M_WORDFIN}`, `$1${M_NOT_WORDFIN}ה`], // חרוץ/ה 77 | ...finals.map( 78 | (f) => [`(${W})${f}${SEP}${toNotFin(f)}ה${B}`, `$1${f}${M_WORDFIN}`, `$1${toNotFin(f)}${M_NOT_WORDFIN}ה`], 79 | ), // מוכן/נה, חרוץ/צה 80 | 81 | [`(${W})ה?${SEP}תה${B}`, '$1ה', '$1תה'], // בכה/תה, רצ/תה 82 | [`(${W})יו${SEP}י?ה${B}`, '$1יו', '$1יה'], // מחקריו/יה 83 | [`(${W})ה${SEP}ית${B}`, '$1ה', '$1ית'], // מומחה/ית 84 | [`(${W})(ו?)י${SEP}ות${B}`, '$1$2י', '$1ות'], // מומחי/ות, שווי/ות 85 | [`(${W})ות${SEP}י${B}`, '$1י', '$1ות'], // מומחות/י 86 | [`(${W})${SEP}ית${B}`, `$1${M_WORDFIN}`, `$1${M_NOT_WORDFIN}ית`], // סטודנט/ית 87 | 88 | [`(${W})י${SEP}תי${B}`, '$1י', '$1תי'], // יקירי/תי 89 | 90 | [`(${W}{4,})אים${SEP}י?ות${B}`, '$1אים', '$1איות'], // ארגנטינאים/ות 91 | 92 | [`(${W})ווים${SEP}?ות${B}`, '$1ווים', '$1וות'], // שווים/ות 93 | [`(${W})וות${SEP}ים${B}`, '$1ווים', '$1וות'], // שוות/ים 94 | [`(${W})(י)?ים${SEP}?(י)?ות${B}`, '$1$2ים', '$1$2$3ות'], // מורים/ות 95 | [`(${W})(י)?ות${SEP}י?ים${B}`, '$1$2ים', '$1$2ות'], // מורות/ים 96 | [`(${W})י${SEP}ות${B}`, '$1י', '$1ות'], // עורכי/ות 97 | 98 | [`(${W})ה${SEP}י${SEP}ו${B}`, '$1ה', '$1י', '$1ו'], // ראה/י/ו 99 | [`(${W})ה${SEP}י${B}`, '$1ה', '$1י'], // ראה/י 100 | [`(${W})י${SEP}ה${SEP}ו${B}`, '$1ה', '$1י', '$1ו'], // ראי/ה/ו 101 | [`(${W})י${SEP}ה${B}`, '$1ה', '$1י'], // ראי/ה 102 | [`(${W}+)\\(י\\)(${W})${SEP}י${SEP}ו${B}`, '$1$2', '$1י$2י', '$1י$2ו'], // הפע(י)ל/י/ו 103 | [`(${W}+)\\(י\\)(${W})${SEP}י${B}`, '$1$2', '$1י$2י'], // הפע(י)ל/י 104 | [`(${HEB})ו(ו?)(${W})${SEP}י${SEP}ו${B}`, `$1$2ו$3${M_WORDFIN}`, `$1$2$2$3${M_NOT_WORDFIN}י`, `$1$2$3${M_NOT_WORDFIN}ו`], // כתוב/י/ו, דווח/י/ו 105 | [`(${HEB})ו(ו?)(${W})${SEP}י${B}`, `$1$2ו$3${M_WORDFIN}`, `$1$2$2$3${M_NOT_WORDFIN}י`], // כתוב/י, דווח/י 106 | [`(${W})${SEP}י${SEP}ו${B}`, `$1${M_WORDFIN}`, `$1${M_NOT_WORDFIN}י`, `$1${M_NOT_WORDFIN}ו`], // לך/י/ו 107 | [`(${W})${SEP}י${B}`, `$1${M_WORDFIN}`, `$1${M_NOT_WORDFIN}י`], // לך/י 108 | 109 | [`(${W})(ה)?${SEP}ת${B}`, `$1$2${M_WORDFIN}`, `$1${M_NOT_WORDFIN}ת`], // נהג/ת, רואה/ת חשבון 110 | 111 | [`(${W})ם${SEP}?ן${B}`, '$1ם', '$1ן'], // אתם/ן 112 | [`(${W})ן${SEP}?ם${B}`, '$1ם', '$1ן'], // אתן/ם 113 | [`ה(${W}+)י(${W})ו${SEP}נה${B}`, 'ה$1י$2ו', 'ה$1$2נה'], // הלבישו/נה 114 | [`(${W}+)ו${SEP}ת(${W}+)נה${B}`, '$1ו', 'ת$2נה'], // יצאו/תצאנה 115 | [`ת(${W}+)ו${SEP}נה${B}`, 'ת$1ו', 'ת$1נה'], // תדרכו/נה 116 | [`(${W}+)ו${SEP}נה${B}`, '$1ו', '$1נה'], // רקדו/נה 117 | 118 | // Parentheses 119 | [`(${W}+)\\(([ותי]{1,3})\\)([יוהםן]{1,3})${B}`, '$1$3', '$1$2$3'], // מתנגד(ות)יו, מתנגד(ות)יהם 120 | [`(${W}+)י\\(י\\)(${W}*)(${FIN})${B}`, '$1י$2$3', '$1יי$2$3'], // פני(י)ך 121 | [`(${W}+)\\(י\\)י(${W}*)(${FIN})${B}`, '$1י$2$3', '$1יי$2$3'], // פנ(י)יך 122 | [`\\(א\\)נשים${B}`, 'אנשים', 'נשים'], // (א)נשים 123 | [`(${W}+)ב\\(ת\\)${B}`, '$1ב', '$1בת'], // חושב(ת) 124 | [`(${W}+)(ך|כ)\\(([םן.\\/]{1,3})\\)${B}`, '$1ך', '$1ך', '$1כ$3'], // שלך(ם), שלך(ן) 125 | 126 | // Special Syntax 127 | ['\\[([^|]*?)\\|([^|]*?)\\|([^|]*?)\\]', '$1', '$2', '$3'], // [בן|בת|ילד] 128 | ['\\[([^|]*?)\\|([^|]*?)\\]', '$1', '$2', true], // [בן|בת] 129 | 130 | // Final Letters fixes 131 | unisex(`ץ${M_NOT_WORDFIN}`, 'צ'), // חרוץה 132 | unisex(`ך${M_NOT_WORDFIN}`, 'כ'), // משךי 133 | unisex(`ן${M_NOT_WORDFIN}`, 'נ'), // השעןי 134 | unisex(`ם${M_NOT_WORDFIN}`, 'מ'), // יזםית 135 | unisex(`ף${M_NOT_WORDFIN}`, 'פ'), // פילוסוףית 136 | 137 | unisex(`([^${G}]+)צ${M_WORDFIN}`, '$1ץ'), // חרוצ 138 | unisex(`([^${G}]+)כ${M_WORDFIN}`, '$1ך'), // משוכ 139 | unisex(`([^${G}]+)נ${M_WORDFIN}`, '$1ן'), // השענ 140 | unisex(`([^${G}]+)מ${M_WORDFIN}`, '$1ם'), // יזמ 141 | unisex(`([^${G}]+)פ${M_WORDFIN}`, '$1ף'), // פילוסופ 142 | 143 | // Remove marks 144 | unisex(`[${M_WORDFIN}${M_NOT_WORDFIN}]`, ''), 145 | ].map(regexize); 146 | -------------------------------------------------------------------------------- /src/ui/style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Switch 3 | */ 4 | 5 | @font-face { 6 | font-family: 'Ivritacons'; 7 | font-weight: '400'; 8 | src: url('fonts/ivritacons-alefalefalef.woff2') format("woff2"), url('fonts/ivritacons-alefalefalef.woff') format("woff"); 9 | } 10 | 11 | $ivrita-accent: #6306ec; 12 | 13 | .ivrita-switch *, .ivrita-mode-changer * { 14 | margin: 0; 15 | padding: 0; 16 | outline: 0; 17 | font-size: 30px; 18 | font-weight: normal; 19 | font-style: normal; 20 | border: 0; 21 | text-decoration: none; 22 | list-style-type: none; 23 | min-width: auto; 24 | min-height: auto; 25 | max-width: none; 26 | max-height: none; 27 | -webkit-text-stroke: none; 28 | -moz-text-stroke: initial; 29 | -ms-text-stroke: initial; 30 | word-spacing: normal; 31 | text-align: left; 32 | width: auto; 33 | height: auto; 34 | position: static; 35 | display: inline-block; 36 | border: 0; 37 | float: none; 38 | background: none; 39 | border-radius: 0; 40 | box-shadow: none; 41 | direction: ltr; 42 | visibility: visible; 43 | opacity: 1; 44 | text-shadow: none; 45 | outline: 0; 46 | vertical-align: unset; 47 | white-space: normal; 48 | letter-spacing: 0; 49 | } 50 | 51 | .ivrita-switch { 52 | z-index: 999999; 53 | background-color: rgba(255, 255, 255, 0.8); 54 | position: fixed; 55 | top: calc(50% - 1em); 56 | left: -1px; 57 | border-radius: 0 7px 7px 0; 58 | transition: .2s all ease-out .1s; 59 | border: 1.1px solid #222; 60 | font-size: 30px; 61 | min-width: 40px; 62 | padding: 3px 0 0; 63 | overflow: hidden; 64 | 65 | *, &:before, &:after { 66 | box-sizing: border-box; 67 | -webkit-box-sizing: border-box; 68 | -moz-box-sizing: border-box; 69 | } 70 | 71 | &.ivrita-switch--right { 72 | left: auto; 73 | right: -1px; 74 | border-radius: 7px 0 0 7px; 75 | } 76 | 77 | a { 78 | font-family: 'Ivritacons' !important; 79 | font-weight: normal !important; 80 | font-style: normal !important; 81 | transition: .1s all ease-out; 82 | display: block; 83 | color: #2d2828; 84 | line-height: 1em; 85 | -webkit-font-smoothing: antialiased; 86 | -moz-osx-font-smoothing: grayscale; 87 | text-align: center; 88 | width: 33px; 89 | height: 33px; 90 | line-height: 33px; 91 | margin: 0 3px; 92 | overflow: hidden; 93 | text-decoration: none !important; 94 | 95 | &.ivrita-logo { 96 | transition: .1s all ease-out; 97 | } 98 | 99 | &.ivrita-button { 100 | visibility: hidden; 101 | border-radius: 5px; 102 | height: 0; 103 | transition: .2s all ease-out; 104 | overflow: hidden; 105 | } 106 | 107 | &.ivrita-button { 108 | &.ivrita-active:not(.ivrita-button-style-0) { 109 | background: #555; 110 | color: #fff; 111 | } 112 | 113 | &.ivrita-button-style-0.ivrita-active { 114 | -webkit-font-feature-settings: "swsh"; 115 | font-feature-settings: "swsh"; 116 | } 117 | 118 | @for $i from 1 through 7 { 119 | &.ivrita-button-style-#{$i} { 120 | -webkit-font-feature-settings: "ss0#{$i}"; 121 | font-feature-settings: "ss0#{$i}"; 122 | } 123 | } 124 | 125 | &:hover, &:focus { 126 | background-color: rgba(0, 0, 0, 0.1); 127 | color: $ivrita-accent; 128 | } 129 | 130 | &.ivrita-active:focus { 131 | border: 1px solid $ivrita-accent; 132 | } 133 | } 134 | 135 | &.ivrita-info-link { 136 | transition: .2s all ease-out; 137 | visibility: hidden; 138 | font-size: 20px; 139 | line-height: 21px; 140 | height: 0; 141 | width: 100%; 142 | 143 | &:hover, &:focus { 144 | color: $ivrita-accent; 145 | -webkit-font-feature-settings: "ss01"; 146 | font-feature-settings: "ss01"; 147 | } 148 | } 149 | } 150 | 151 | 152 | &:hover, &:focus, &:focus-within { 153 | top: calc(50% - 66px); 154 | 155 | a.ivrita-logo { 156 | height: 0; 157 | visibility: hidden; 158 | } 159 | 160 | a.ivrita-button { 161 | visibility: visible; 162 | font-size: 30px; 163 | height: 33px; 164 | } 165 | 166 | a.ivrita-info-link { 167 | border-top: 1px solid #555; 168 | margin: 4px 0 0; 169 | height: auto; 170 | visibility: visible; 171 | } 172 | } 173 | } 174 | 175 | 176 | /** 177 | * Toolbar 178 | */ 179 | 180 | .ivrita-toolbar { 181 | * { 182 | display: inline-block !important; 183 | } 184 | 185 | font-size: 16px; 186 | border: 1px solid rgba(0, 0, 0, 0.15); 187 | display: -webkit-box; 188 | display: -ms-flexbox; 189 | display: flex; 190 | -webkit-box-pack: justify; 191 | -ms-flex-pack: justify; 192 | justify-content: space-between; 193 | margin-bottom: 1.6em !important; 194 | padding: 0.5em 0.7em; 195 | overflow: auto; 196 | position: relative; 197 | align-items: center; 198 | background: rgba(0, 0, 0, 0.03); 199 | color: #333333; 200 | font-size: 18px; 201 | z-index: 1; 202 | 203 | .ivrita-toolbar-label { 204 | padding-left: 0.2em; 205 | opacity: 0.7; 206 | line-height: 1em; 207 | } 208 | 209 | a { 210 | color: #333333 !important; 211 | text-decoration: none !important; 212 | border: none !important; 213 | box-shadow: none !important; 214 | -webkit-box-shadow: none !important; 215 | transition: color .3s !important; 216 | display: inline-block !important; 217 | padding: 0 0.3em !important; 218 | margin: 1px !important; 219 | white-space: nowrap; 220 | cursor: pointer !important; 221 | 222 | &:before { 223 | content: attr(data-ivrita-icon) !important; 224 | font-size: 1.2em !important; 225 | vertical-align: middle !important; 226 | font-family: 'Ivritacons' !important; 227 | } 228 | 229 | &:focus, &:hover { 230 | border: 1px solid rgba(0, 0, 0, 0.2) !important; 231 | margin: 0 !important; 232 | } 233 | 234 | &.ivrita-mode-changer { 235 | border-radius: 2px !important; 236 | } 237 | 238 | &.ivrita-active { 239 | border: 1px solid rgba(0, 0, 0, 0.8) !important; 240 | margin: 0 !important; 241 | } 242 | 243 | &.ivrita-toolbar-info { 244 | padding: 0 !important; 245 | margin: 0 !important; 246 | line-height: 1em; 247 | 248 | &:hover { 249 | border: none !important; 250 | 251 | &:before { 252 | -webkit-font-feature-settings: "ss01"; 253 | font-feature-settings: "ss01"; 254 | } 255 | } 256 | } 257 | } 258 | } 259 | 260 | /* Mobile CSS */ 261 | @media screen and (max-width: 480px) { 262 | .ivrita-switch { 263 | top: auto; 264 | bottom: 0; 265 | border-bottom: 0; 266 | border-bottom-right-radius: 0px !important; 267 | border-bottom-left-radius: 0px !important; 268 | 269 | &:hover { 270 | top: auto; 271 | } 272 | } 273 | 274 | .ivrita-toolbar { 275 | font-size: 14px; 276 | 277 | .ivrita-toolbar-menu { 278 | display: block !important; 279 | width: 100%; 280 | } 281 | 282 | .ivrita-toolbar-label { 283 | display: block !important; 284 | clear: both; 285 | margin-bottom: 0.3em; 286 | } 287 | 288 | a.ivrita-toolbar-info { 289 | position: absolute !important; 290 | left: 0.8em; 291 | top: 0.4em; 292 | } 293 | } 294 | } -------------------------------------------------------------------------------- /src/wordlists.js: -------------------------------------------------------------------------------- 1 | import { 2 | G, SEP, EXTSEP, B, 3 | } from './utils/characters'; 4 | 5 | export const custom = [ 6 | [`א${EXTSEP}נשים`, 'אנשים', 'נשים'], // א.נשים 7 | [`א${EXTSEP}נש(?:ות|י)${B}`, 'אנשי', 'נשות'], // א.נשי 8 | [`את${SEP}ה`, 'אתה', 'את'], // את/ה 9 | [`איש${SEP}(?:אש)?ת`, 'איש', 'אשת'], // איש/אשת, איש/ת 10 | [`אשת${SEP}איש`, 'איש', 'אשת'], // אשת/איש 11 | [`(גבר|איש)${SEP}אישה`, '$1', 'אישה'], // גבר/אישה, איש/אישה 12 | [`אחי${SEP}ותי${B}`, 'אחי', 'אחותי'], // אחי/ותי 13 | [`אח${SEP}ות${B}`, 'אח', 'אחות'], // אח/ות 14 | [`ל(ו|ה)${SEP}ל(ו|ה)${B}`, 'לו', 'לה'], // לו/לה, לה/לו 15 | [`ב(ן|ת)${SEP}ב(ן|ת)${B}`, 'בן', 'בת'], // בת/בן, בן/בת 16 | [`ה(ו|י)א${SEP}ה(ו|י)א${B}`, 'הוא', 'היא'], // הוא/היא, היא/הוא 17 | [`אנשי${SEP}ות${B}`, 'אנשי', 'נשות'], // אנשי/ות 18 | [`מישהו${SEP}י${B}`, 'מישהו', 'מישהי'], // מישהו/י 19 | [`אחד${SEP}(אח)?ת${B}`, 'אחד', 'אחת'], // אחד/ת, אחד/אחת 20 | [`אחת${SEP}(אח)?ד${B}`, 'אחד', 'אחת'], // אחת/ד, אחת/אחד 21 | [`יקיר(י?)${SEP}תי${B}`, 'יקירי', 'יקירתי'], // יקירי/תי 22 | [`אהוב(י?)${SEP}תי${B}`, 'אהובי', 'אהובתי'], // אהובי/תי 23 | ['סב\\(ת\\)א', 'סבא', 'סבתא'], // סבא/סבתא 24 | [`זה${SEP}זאת${B}`, 'זה', 'זאת'], // זה/זאת 25 | [`זאת${SEP}זה${B}`, 'זה', 'זאת'], // זאת/זה 26 | [`זה${SEP}ז?ו${B}`, 'זה', 'זו'], // זה/זו 27 | [`זו${SEP}ז?ה${B}`, 'זה', 'זו'], // זו/זה 28 | [`נשוי${SEP}א?ה${B}`, 'נשוי', 'נשואה'], // נשוי/אה 29 | [`חשוב${SEP}י${B}`, 'חשוב', 'חשבי'], // חשוב/י (exception because of שוב/י which is in verbsFemaleKeepVav) 30 | [`חשוב${SEP}י${SEP}ו${B}`, 'חשוב', 'חשבי', 'חשבו'], // חשוב/י/ו (exception because of שוב/י which is in verbsFemaleKeepVav) 31 | ]; 32 | 33 | // For most verbs (Unless found in *verbsFemaleKeepVav*), we follow the rules of: 34 | // כתוב/י => Vav before last letter => Vav removed => כתבי 35 | // else: 36 | // לך/י => Yod added after original word => לכי 37 | // However, some verbs need an aditional Yod before their last letter: 38 | // הקשב => Add Yod before and after Bet => הקשיבי 39 | // This is the list of words which need that extra Yod: 40 | export const verbsFemaleExtraYod = [ 41 | 'האר', 42 | 'הבא', 43 | 'הגב', 44 | 'הדגם', 45 | 'הדלק', 46 | 'הוסף', 47 | 'הואל', 48 | 'הורד', 49 | 'הזמן', 50 | 'הזן', 51 | 'הכנס', 52 | 'הלבש', 53 | 'המלץ', 54 | 'המשך', 55 | 'הסר', 56 | 'הסתר', 57 | 'הפעל', 58 | 'הפקד', 59 | 'הצג', 60 | 'הקלד', 61 | 'הקלק', 62 | 'הקש', 63 | 'הקשב', 64 | 'הרגש', 65 | 'הרם', 66 | 'הרעף', 67 | 'השב', 68 | 'השלם', 69 | 'השתק', 70 | 'התמד', 71 | 'התקן', 72 | 'העתק', 73 | 'הדבק', 74 | ]; 75 | 76 | export const verbsFemaleKeepVav = [ 77 | 'קום', 78 | 'רוץ', 79 | 'עופ', 80 | 'שוב', 81 | 'זוז', 82 | 'טוס', 83 | 'שוט', 84 | 'בוא', 85 | ]; 86 | 87 | // Most plurals don't need an extra Yod on their female form: מורים->מורות 88 | // These are the exceptions which need a Yod: 89 | export const pluralsWithExtraYod = [ 90 | '(א|ס)ובי(י?)קטיב', // אוביקטיבי וסוביקטיבי 91 | `אחמ([${G}]?)ש`, 92 | `ח([${G}])כ`, 93 | `מ([${G}])פ`, 94 | `מנכ([${G}]?)ל`, 95 | `מפא([${G}]?)יניק`, 96 | `משת([${G}]?)פ`, 97 | `עו([${G}])(ס|ד)`, 98 | `רו([${G}])ח`, 99 | `רשג([${G}]?)ד`, 100 | 'א(י?)כפת', 101 | 'אביב', 102 | 'אח', 103 | 'אגרונומ', 104 | 'אדריכל', 105 | 'אוטיסט', 106 | 'אוסטר(ל?)', 107 | 'אופטימ', 108 | 'אחרא', 109 | 'אחיינ', 110 | 'איטלק', 111 | 'אינדיבידואליסט', 112 | 'אירונ', 113 | 'אירופא', 114 | 'אכזר', 115 | 'אלגנט', 116 | 'אלכוהוליסט', 117 | 'אלמונ', 118 | 'אמית', 119 | 'אמריק(א|נ)', 120 | 'אנאלפבית', 121 | 'אנגל', 122 | 'אנוש', 123 | 'אנטישמ', 124 | 'אנליסט', 125 | 'אנרכיסט', 126 | 'אסטרולוג', 127 | 'אסיאת', 128 | 'אפריק(נ|א)', 129 | 'אצנ', 130 | 'אקדמ(א?)', 131 | 'אקטואל', 132 | 'אקטיביסט', 133 | 'אקרא', 134 | 'ארטיסט', 135 | 'אשכנז', 136 | 'אתאיסט', 137 | 'אתיופ', 138 | 'בוגדנ', 139 | 'בולגר', 140 | 'בטחונ', 141 | 'ביביסט', 142 | 'ב(י?)דיונ', 143 | 'בינונ', 144 | 'בינלאומ', 145 | 'בל(א?)גניסט', 146 | 'בלוגר', 147 | 'בלונד', 148 | 'במא', 149 | 'ברב(א?)ר', 150 | 'ברונט', 151 | 'בריט', 152 | 'ברמנ', 153 | `ג${G}ובניק`, 154 | `ג${G}ינג${G}`, 155 | 'גות', 156 | 'גיטריסט', 157 | 'גר(א?)פ', 158 | 'גרמנ', 159 | 'גרפיקא', 160 | 'דברנ', 161 | 'דוקטורנט', 162 | 'דושבג', 163 | 'דיאטנ', 164 | 'דינ(א?)מ', 165 | '(י?)הוד', 166 | 'הי(פ?)סטר', 167 | 'היפ', 168 | 'הססנ', 169 | 'הנדסא', 170 | 'הרמונ', 171 | 'וטרינר', 172 | 'זכא', 173 | 'חבר(ו?)ת', 174 | 'חובבנ', 175 | 'חולמנ', 176 | 'חושנ', 177 | 'חילונ', 178 | 'חי(ו|נ)נ', 179 | 'חיפא', 180 | 'חמדנ', 181 | 'חרד', 182 | 'חרד(ת?)', 183 | 'חרמנ', 184 | 'חשמלא', 185 | 'טבח', 186 | 'טבעונ', 187 | 'טורק', 188 | 'טיפוגרפ', 189 | 'טכנא', 190 | 'טרוריסט', 191 | 'טרמפיסט', 192 | 'טרנס', 193 | 'ידידות', 194 | 'יוגיסט', 195 | 'יוונ', 196 | 'יורקר', 197 | 'יזמ', 198 | 'ימא', 199 | 'ימ(י?)נ', 200 | 'ירושלמ', 201 | 'ישראל', 202 | 'כימא', 203 | 'כלכלנ', 204 | 'כרונ', 205 | 'לבנונ', 206 | 'לוחמנ', 207 | 'ליברל', 208 | 'ליכודניק', 209 | 'מאסטר', 210 | 'מוזיק(ל|א)', 211 | 'מומח', 212 | 'מזוכיסט', 213 | 'מזרח', 214 | 'מחזא', 215 | 'מטאליסט', 216 | 'מטרידנ', 217 | 'מילואימניק', 218 | 'מיליארדר', 219 | 'מיליונר', 220 | 'מכונא', 221 | 'מלאכ', 222 | 'מלצר', 223 | 'מפסידנ', 224 | 'מצליחנ', 225 | 'מצפונ', 226 | 'מקצוע(נ?)', 227 | 'מרדנ', 228 | 'מרקסיסט', 229 | 'נגר', 230 | 'נובוריש', 231 | 'נודיסט', 232 | 'נודניק', 233 | 'נוצר', 234 | 'נורא', 235 | 'נורווג', 236 | 'נטורופת', 237 | 'נרקומנ', 238 | 'ס(א?)דיסט', 239 | 'ס(א?)ח', 240 | 'סדרנ', 241 | 'סהרור', 242 | 'סוליד(ר?)', 243 | 'סוציאליסט', 244 | 'סטודנט', 245 | 'סטרייט', 246 | 'סמכות', 247 | 'סנדלר', 248 | 'סנוב', 249 | 'ססגונ', 250 | 'ספונטנ', 251 | 'ספורטיב', 252 | 'ספציפ', 253 | 'ספרד', 254 | 'סקסולוג', 255 | 'סרב', 256 | 'סרטט', 257 | 'עירונ', 258 | 'עיתונא', 259 | 'עממ', 260 | 'עניינ', 261 | 'ענק', 262 | 'עסיס', 263 | 'עצמא', 264 | 'עקרונ', 265 | 'ערב', 266 | 'ערס', 267 | 'פאנקיסט', 268 | 'פדופיל', 269 | 'פוליטיקא', 270 | 'פולנ', 271 | 'פופול(א?)ר', 272 | 'פופוליסט', 273 | 'פחדנ', 274 | 'פטריוט', 275 | 'פילוסופ', 276 | 'פיזיוטרפיסט', 277 | 'פמיניסט', 278 | 'פסיכופת', 279 | 'פסיכולוג', 280 | 'פסיכיאטר', 281 | 'פסנתרנ', 282 | 'פציפיסט', 283 | 'פריק', 284 | 'פרופסור', 285 | 'פרזנטור', 286 | 'פריל(א?)נסר', 287 | 'פרסומא', 288 | 'פקח', 289 | 'פשיסט', 290 | 'צבע', 291 | 'צבעונ', 292 | 'צי(ו?)נ', 293 | 'ציבור', 294 | 'ציפלונ', 295 | 'צמחונ', 296 | 'צפונ', 297 | 'צרפת', 298 | 'קדמונ', 299 | 'קוויר', 300 | 'קומוניסט', 301 | 'קומיק(ס?)א', 302 | 'קונדיטור', 303 | 'קוסמטיקא', 304 | 'קופא', 305 | 'קוקסינל', 306 | 'קטלנ', 307 | 'קטנונ', 308 | 'קיבוצניק', 309 | 'קיצונ', 310 | 'קלאס', 311 | 'קלדנ', 312 | 'קלפטומנ', 313 | 'קניינ', 314 | 'קפדנ', 315 | 'קפיטליסט', 316 | 'קריקטוריסט', 317 | 'קצב', 318 | 'רבנ', 319 | 'רוחנ', 320 | 'רוס', 321 | 'רוקיסט', 322 | 'רמא', 323 | 'רפד', 324 | 'רקדנ', 325 | 'ש(ו?)ויונ', 326 | 'שאפתנ', 327 | 'שוביניסט', 328 | 'שווד', 329 | 'שוויצר', 330 | 'שחיינ', 331 | 'שחקנ', 332 | 'שלומיאל', 333 | 'שמאלנ', 334 | 'שמנמנ', 335 | 'שמרנ', 336 | 'שפ', //* 337 | 'שק(ר|ד)נ', 338 | 'שרמנט', 339 | 'תורכ', 340 | 'תזונא', 341 | 'תחמנ', 342 | 'תסריטא', 343 | 'תצפיתנ', 344 | 'תקציבא', 345 | 'תרבות', 346 | ]; 347 | -------------------------------------------------------------------------------- /src/element.js: -------------------------------------------------------------------------------- 1 | import { 2 | MALE, FEMALE, NEUTRAL, ORIGINAL, GENDERS, genderize, 3 | } from './ivrita'; 4 | import TextAttribute from './textAttribute'; 5 | import TextElement, { MALE_DATA_ATTR, FEMALE_DATA_ATTR, NEUTRAL_DATA_ATTR } from './textElement'; 6 | import TextNode from './textNode'; 7 | import TextObject from './textObject'; 8 | import { HEB, SYNTAX } from './utils/characters'; 9 | 10 | const hebrewRegex = new RegExp(HEB); 11 | const ivritaSyntaxRegex = new RegExp(SYNTAX); 12 | 13 | const MULTI = GENDERS.length; // ENUM-like 14 | 15 | export default class IvritaElement { 16 | static EVENT_MODE_CHANGED = 'ivrita-mode-changed'; 17 | 18 | static ORIGINAL = ORIGINAL; 19 | 20 | static MALE = MALE; 21 | 22 | static FEMALE = FEMALE; 23 | 24 | static NEUTRAL = NEUTRAL; 25 | 26 | // MULTI is a special with FFS enabled, but is essentialy the NEUTRAL mode. 27 | static MULTI = MULTI; 28 | 29 | static GENDERS = [...GENDERS, MULTI]; 30 | 31 | static instances = new Map(); 32 | 33 | static defaultMode = NEUTRAL; 34 | 35 | static genderize = genderize; 36 | 37 | static textObjects = TextObject.instances; 38 | 39 | nodes = new Set(); 40 | 41 | elements = []; 42 | 43 | mode; 44 | 45 | fontFeatureSettings; 46 | 47 | relavantAttributes = { 48 | 'a, img, button, input': ['title'], 49 | [`input:not([type=${['submit', 'button', 'checkbox', 'radio', 'hidden', 'image', 'range', 'reset', 'file'].join(']):not([type=')}])`]: ['placeholder'], 50 | 'input[type=submit], input[type=button], input[type=reset]': ['value'], 51 | }; 52 | 53 | constructor(elem, mode) { 54 | if (typeof elem === 'undefined') { 55 | this.elements = [document.body]; 56 | const titleTag = document.documentElement.querySelector('title'); 57 | if (titleTag) { 58 | this.elements.push(titleTag); 59 | } 60 | } else if (elem instanceof Array && elem.filter((el) => el instanceof HTMLElement)) { 61 | this.elements = elem; 62 | } else if (elem instanceof NodeList) { 63 | this.elements = Array.from(elem); 64 | } else if (elem instanceof HTMLElement) { 65 | this.elements = [elem]; 66 | } else if (typeof jQuery === 'function' && elem instanceof jQuery && typeof elem.toArray === 'function') { 67 | this.elements = elem.toArray(); 68 | } else { 69 | throw new Error('Passed argument is not an HTMLElement.'); 70 | } 71 | 72 | if (this.elements.length === 1 && this.constructor.instances.has(this.elements[0])) { 73 | const preExistingInstance = this.constructor.instances.get(this.elements[0]); 74 | preExistingInstance.registerTextObjects(this.elements[0]); // Make sure nodes are registered 75 | return preExistingInstance; 76 | } 77 | 78 | this.observer = new MutationObserver(this.onElementChange.bind(this)); 79 | this.elements.forEach((el) => { 80 | this.observer.observe(el, { 81 | childList: true, 82 | subtree: true, 83 | characterData: false, 84 | }); 85 | this.constructor.instances.set(el, this); 86 | this.registerTextObjects(el); 87 | }); 88 | 89 | if (typeof mode !== 'undefined') { 90 | this.setMode(mode); 91 | } else if (this.constructor.defaultMode) { 92 | this.setMode(this.constructor.defaultMode); 93 | } 94 | } 95 | 96 | destroy() { 97 | this.setMode(ORIGINAL); 98 | this.setFontFeatureSettings(false); 99 | if (this.observer) { 100 | this.observer.disconnect(); 101 | } 102 | this.nodes.clear(); 103 | this.elements.forEach((el) => { 104 | this.constructor.instances.delete(el); 105 | }); 106 | } 107 | 108 | static setDefaultMode(newMode) { 109 | this.defaultMode = newMode; 110 | } 111 | 112 | setMode(newMode = NEUTRAL) { 113 | if (!this.constructor.GENDERS.includes(newMode)) { 114 | return this; 115 | } 116 | 117 | if ((this.mode === MULTI && newMode !== MULTI) || (this.mode !== MULTI && newMode === MULTI)) { 118 | this.setFontFeatureSettings(newMode === MULTI); 119 | } 120 | 121 | this.mode = newMode; 122 | // If the new mode is MULTI, mask it from the child nodes - for them it's a NEUTRAL mode. 123 | this.nodes.forEach((node) => node.setMode(newMode === MULTI ? NEUTRAL : newMode)); 124 | 125 | this.dispatchModeChangedEvent(newMode); 126 | 127 | return this; 128 | } 129 | 130 | static setMode(newMode) { 131 | this.instances.forEach((instance) => instance.setMode(newMode)); 132 | } 133 | 134 | genderize(text) { 135 | return this.constructor.genderize(text, this.mode); 136 | } 137 | 138 | dispatchModeChangedEvent(mode = this.mode) { 139 | this.elements.forEach( 140 | (el) => el.dispatchEvent(new CustomEvent(this.constructor.EVENT_MODE_CHANGED, 141 | { bubbles: true, detail: { mode, firingInstance: this } })), 142 | ); 143 | } 144 | 145 | registerTextObjects(element) { 146 | this.registerTextNodes(element); 147 | this.registerTextAttributes(element); 148 | } 149 | 150 | registerTextNodes(element) { 151 | let currentNode; 152 | const walk = document.createTreeWalker( 153 | element, 154 | NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, 155 | { 156 | acceptNode: (node) => (this.constructor.acceptNodeFilter(node)), 157 | }, 158 | ); 159 | 160 | // eslint-disable-next-line no-cond-assign 161 | while ((currentNode = walk.nextNode())) { 162 | let newNode; 163 | if (currentNode instanceof Text) { 164 | newNode = new TextNode(currentNode); 165 | } else if (currentNode instanceof Element) { 166 | newNode = new TextElement(currentNode); 167 | } 168 | this.nodes.add(newNode); 169 | // Set the new node's mode to the current mode, to get in line with everything else 170 | if (this.mode) { 171 | newNode.setMode(this.mode); 172 | } 173 | } 174 | } 175 | 176 | registerTextAttributes(element) { 177 | Object.entries(this.relavantAttributes).forEach(([selector, attributes]) => { 178 | Array.from(element.querySelectorAll(selector)).forEach((input) => { 179 | attributes.forEach((attrName) => { 180 | if (input.hasAttribute(attrName)) { 181 | this.nodes.add(new TextAttribute(input.getAttributeNode(attrName))); 182 | } 183 | }); 184 | }); 185 | }); 186 | } 187 | 188 | static acceptNodeFilter(node) { 189 | if (TextObject.instances.has(node)) { // Already indexed, will be a pointer to existing node 190 | return NodeFilter.FILTER_ACCEPT; 191 | } 192 | 193 | if (node.textContent.trim().length <= 0) { 194 | return NodeFilter.FILTER_REJECT; // If there's no content, reject all child nodes 195 | } 196 | 197 | if (node.nodeType === Node.ELEMENT_NODE) { 198 | if (node.dataset.ivritaDisable) { 199 | return NodeFilter.FILTER_REJECT; 200 | } 201 | if ([MALE_DATA_ATTR, FEMALE_DATA_ATTR, NEUTRAL_DATA_ATTR] 202 | .filter((attr) => node.dataset[attr]).length) { 203 | return NodeFilter.FILTER_ACCEPT; 204 | } 205 | } else if (node.nodeType === Node.TEXT_NODE) { 206 | if (hebrewRegex.test(node.textContent) // Test for Hebrew Letters 207 | && ivritaSyntaxRegex.test(node.textContent)) { 208 | return NodeFilter.FILTER_ACCEPT; 209 | } 210 | } 211 | return NodeFilter.FILTER_SKIP; 212 | } 213 | 214 | onElementChange(mutationsList) { 215 | const closest = (el, s) => { 216 | do { 217 | if (el instanceof Element && Element.prototype.matches.call(el, s)) return el; 218 | // eslint-disable-next-line no-param-reassign 219 | el = el.parentElement || el.parentNode; 220 | } while (el !== null && el.nodeType === 1); 221 | return null; 222 | }; 223 | 224 | mutationsList.forEach((mutation) => { 225 | if (mutation.type === 'childList') { 226 | const added = Array.from(mutation.addedNodes); 227 | const removed = Array.from(mutation.removedNodes); 228 | if (added.length === removed.length) { // Probably just changed, not really removed 229 | removed.forEach((oldNode, i) => { 230 | if (oldNode.nodeType === Node.TEXT_NODE) { 231 | const newNode = added[i]; 232 | if (TextNode.instances.has(oldNode) && newNode.nodeType === Node.TEXT_NODE) { 233 | const nodeObj = TextNode.instances.get(oldNode); 234 | nodeObj.node = newNode; // This is dangerous, make sure it makes sense 235 | TextNode.instances.set(newNode, nodeObj); 236 | TextNode.instances.delete(oldNode); 237 | } 238 | } // TODO: what about nodes with nested text nodes? 239 | }); 240 | } else { 241 | added.forEach((node) => { 242 | if (closest(node, '[data-ivrita-disable]')) { 243 | return; 244 | } 245 | if (node.nodeType === Node.TEXT_NODE 246 | && this.constructor.acceptNodeFilter(node) === NodeFilter.FILTER_ACCEPT) { 247 | const ivritaTextNode = new TextNode(node); 248 | this.nodes.add(ivritaTextNode); 249 | // Set the new node's mode to the current mode, to get in line with everything else 250 | if (this.mode) { 251 | ivritaTextNode.setMode(this.mode); 252 | } 253 | } else if (node.childNodes.length > 0) { 254 | this.registerTextNodes(node); 255 | } 256 | }); 257 | } 258 | } 259 | }); 260 | } 261 | 262 | setFontFeatureSettings(isActive) { 263 | this.fontFeatureSettings = isActive; 264 | this.elements.forEach((el) => { 265 | const originalFFS = el.style.fontFeatureSettings; 266 | let result = originalFFS.slice().replace('normal', ''); 267 | 268 | if (isActive) { 269 | if (!result.includes('titl')) { 270 | if (result) { // Only add a space if property exists 271 | result += ', '; 272 | } 273 | result += '"titl"'; 274 | } 275 | } else if (result.includes('titl')) { 276 | result = result.replace(/(, )?"?'?titl"?'?/, ''); 277 | } 278 | 279 | if (!result) result = 'normal'; 280 | 281 | el.style.fontFeatureSettings = result; 282 | }); 283 | 284 | return this; 285 | } 286 | } 287 | 288 | if (typeof jQuery === 'function') { 289 | jQuery.fn.ivrita = function ivritajQueryFn(gender) { 290 | return new IvritaElement(this, gender); 291 | }; 292 | } 293 | -------------------------------------------------------------------------------- /test/dom.test.js: -------------------------------------------------------------------------------- 1 | import Ivrita from '../src/element'; 2 | import TextObject from '../src/textObject'; 3 | 4 | import { 5 | FEMALE, MALE, NEUTRAL, ORIGINAL, 6 | } from '../src/ivrita'; 7 | 8 | function waitEventLoop() { 9 | return new Promise((resolve) => setImmediate(resolve)); 10 | } 11 | 12 | const template = ` 13 | <div id="content"> 14 | <p>[מעצבים|מתכנתות|הייטקיסטים] רבים/ות <u>מרגישים/ות</u> <i>תסכול</i>, כאשר <b>פונים/ות</b> אליהם/ן שלא בשפתם/ן.</p> 15 | </div> 16 | `; 17 | 18 | test('DOM plug-in', () => { 19 | document.body.innerHTML = template; 20 | const paragraph = document.querySelector('#content p'); 21 | 22 | const ivrita = new Ivrita(); 23 | 24 | // Female 25 | ivrita.setMode(FEMALE); 26 | expect(paragraph.innerHTML).toBe('מתכנתות רבות <u>מרגישות</u> <i>תסכול</i>, כאשר <b>פונות</b> אליהן שלא בשפתן.'); 27 | 28 | // Male 29 | ivrita.setMode(MALE); 30 | expect(paragraph.innerHTML).toBe('מעצבים רבים <u>מרגישים</u> <i>תסכול</i>, כאשר <b>פונים</b> אליהם שלא בשפתם.'); 31 | 32 | // Neutral 33 | ivrita.setMode(NEUTRAL); 34 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות <u>מרגישים/ות</u> <i>תסכול</i>, כאשר <b>פונים/ות</b> אליהם/ן שלא בשפתם/ן.'); 35 | 36 | // Back to original 37 | ivrita.setMode(ORIGINAL); 38 | expect(paragraph.innerHTML).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות <u>מרגישים/ות</u> <i>תסכול</i>, כאשר <b>פונים/ות</b> אליהם/ן שלא בשפתם/ן.'); 39 | 40 | // Back to male 41 | ivrita.setMode(MALE); 42 | expect(paragraph.innerHTML).toBe('מעצבים רבים <u>מרגישים</u> <i>תסכול</i>, כאשר <b>פונים</b> אליהם שלא בשפתם.'); 43 | 44 | // Destroy 45 | ivrita.destroy(); 46 | expect(paragraph.innerHTML).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות <u>מרגישים/ות</u> <i>תסכול</i>, כאשר <b>פונים/ות</b> אליהם/ן שלא בשפתם/ן.'); 47 | 48 | expect(ivrita.nodes.size).toEqual(0); 49 | }); 50 | 51 | test('Default mode is working', () => { 52 | document.body.innerHTML = template; 53 | const paragraph = document.querySelector('#content p'); 54 | 55 | new Ivrita(); 56 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות <u>מרגישים/ות</u> <i>תסכול</i>, כאשר <b>פונים/ות</b> אליהם/ן שלא בשפתם/ן.'); 57 | }); 58 | 59 | test('Single element passed to constructor', () => { 60 | document.body.innerHTML = template; 61 | const ivrita = new Ivrita(document.querySelector('#content')); 62 | const bold = document.querySelector('#content p b'); 63 | 64 | ivrita.setMode(MALE); 65 | expect(bold.innerHTML).toBe('פונים'); 66 | }); 67 | 68 | test('Multiple elements passed to constructor', () => { 69 | document.body.innerHTML = template; 70 | const ivrita = new Ivrita(document.querySelectorAll('b, u, i')); 71 | const bold = document.querySelector('#content p b'); 72 | const underlined = document.querySelector('#content p u'); 73 | const italic = document.querySelector('#content p i'); 74 | 75 | ivrita.setMode(MALE); 76 | expect(bold.innerHTML).toBe('פונים'); 77 | expect(underlined.innerHTML).toBe('מרגישים'); 78 | expect(italic.innerHTML).toBe('תסכול'); 79 | expect(document.body.querySelector('#content p').textContent).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים תסכול, כאשר פונים אליהם/ן שלא בשפתם/ן.'); 80 | }); 81 | 82 | test('Array of elements passed to constructor', () => { 83 | document.body.innerHTML = template; 84 | const bold = document.querySelector('#content p b'); 85 | const underlined = document.querySelector('#content p u'); 86 | const italic = document.querySelector('#content p i'); 87 | const ivrita = new Ivrita([bold, underlined, italic]); 88 | 89 | ivrita.setMode(MALE); 90 | expect(bold.innerHTML).toBe('פונים'); 91 | expect(underlined.innerHTML).toBe('מרגישים'); 92 | expect(italic.innerHTML).toBe('תסכול'); 93 | }); 94 | 95 | test('<title> tag is changed', () => { 96 | document.documentElement.innerHTML = ` 97 | <html> 98 | <head> 99 | <title>צור/י קשר 100 | 101 | 102 | ${template} 103 | 104 | `; 105 | 106 | const title = document.documentElement.querySelector('title'); 107 | const ivrita = new Ivrita(); 108 | 109 | ivrita.setMode(FEMALE); 110 | expect(title.innerHTML).toBe('צרי קשר'); 111 | }); 112 | 113 | test('Ovserver catches new elements added and sets their mode properly', async () => { 114 | document.body.innerHTML = template; 115 | const i = new Ivrita(document.body); 116 | 117 | i.setMode(FEMALE); 118 | 119 | document.body.insertAdjacentHTML('beforeend', 'את/ה נהדר/ת'); 120 | 121 | await waitEventLoop(); // Required to activate MutationObserver 122 | 123 | expect(document.querySelector('#content u').innerHTML).toBe('מרגישות'); 124 | expect(document.querySelector('span').innerHTML).toBe('את נהדרת'); 125 | 126 | i.setMode(MALE); 127 | 128 | expect(document.querySelector('#content u').innerHTML).toBe('מרגישים'); 129 | expect(document.querySelector('span').innerHTML).toBe('אתה נהדר'); 130 | }); 131 | 132 | test('jQuery element passed to constructor', () => { 133 | document.body.innerHTML = template; 134 | const ivrita = new Ivrita(jQuery('#content')); 135 | const bold = document.querySelector('#content p b'); 136 | 137 | ivrita.setMode(MALE); 138 | expect(bold.innerHTML).toBe('פונים'); 139 | }); 140 | 141 | test('jQuery function', () => { 142 | document.body.innerHTML = template; 143 | 144 | const ivrita = jQuery('#content').ivrita(); 145 | const bold = document.querySelector('#content p b'); 146 | 147 | ivrita.setMode(MALE); 148 | expect(bold.innerHTML).toBe('פונים'); 149 | }); 150 | 151 | test('jQuery function with gender', () => { 152 | document.body.innerHTML = template; 153 | 154 | jQuery('#content p b').ivrita(MALE); 155 | const p = document.querySelector('#content p'); 156 | 157 | expect(p.textContent).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים אליהם/ן שלא בשפתם/ן.'); 158 | }); 159 | 160 | test('Bad element passed to constructor', () => { 161 | document.body.innerHTML = template; 162 | expect(() => { 163 | new Ivrita('#content'); // Should be DOMElement, not string 164 | }).toThrow(Error); 165 | }); 166 | 167 | test('Node singletons', () => { 168 | const textNodeRegister = jest.spyOn(TextObject.instances, 'set'); 169 | 170 | document.body.innerHTML = template; 171 | const ivrita = new Ivrita(document.querySelector('#content')); 172 | ivrita.setMode(FEMALE); 173 | 174 | const bold = document.querySelector('#content p b'); 175 | const ivrita2 = new Ivrita(bold); 176 | ivrita2.setMode(MALE); 177 | expect(document.getElementById('content').textContent.trim()).toBe('מתכנתות רבות מרגישות תסכול, כאשר פונים אליהן שלא בשפתן.'); 178 | expect(ivrita.nodes.size).toEqual(4); 179 | expect(ivrita2.nodes.size).toEqual(1); 180 | expect(textNodeRegister).toHaveBeenCalledTimes(4); 181 | }); 182 | 183 | test('Events', () => { 184 | const listener = jest.fn(); 185 | 186 | document.body.innerHTML = template; 187 | const ivrita = new Ivrita(document.querySelector('#content')); 188 | 189 | document.addEventListener(Ivrita.EVENT_MODE_CHANGED, listener); 190 | 191 | ivrita.setMode(MALE); 192 | expect(listener.mock.calls.length).toBe(1); 193 | expect(listener.mock.calls[0][0].detail.mode).toBe(MALE); 194 | 195 | ivrita.setMode(FEMALE); 196 | expect(listener.mock.calls.length).toBe(2); 197 | expect(listener.mock.calls[1][0].detail.mode).toBe(FEMALE); 198 | }); 199 | 200 | test('data-ivrita-disable', () => { 201 | document.body.innerHTML = '

[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.

'; 202 | const paragraph = document.querySelector('p'); 203 | 204 | const ivrita = new Ivrita(); 205 | 206 | ivrita.setMode(FEMALE); 207 | expect(paragraph.innerHTML).toBe('מתכנתות רבות מרגישים/ות תסכול, כאשר פונות אליהן שלא בשפתן.'); 208 | }); 209 | 210 | test('data-ivrita-male', () => { 211 | document.body.innerHTML = '

[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.

'; 212 | const paragraph = document.querySelector('p'); 213 | 214 | const ivrita = new Ivrita(); 215 | 216 | ivrita.setMode(FEMALE); 217 | expect(paragraph.innerHTML).toBe('מתכנתות רבות מרגישות תסכול, כאשר פונות אליהן שלא בשפתן.'); 218 | 219 | ivrita.setMode(MALE); 220 | expect(paragraph.innerHTML).toBe('מעצבים רבים חשים תסכול, כאשר פונים אליהם שלא בשפתם.'); 221 | 222 | ivrita.setMode(NEUTRAL); 223 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.'); 224 | }); 225 | 226 | test('data-ivrita-female', () => { 227 | document.body.innerHTML = '

[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.

'; 228 | const paragraph = document.querySelector('p'); 229 | 230 | const ivrita = new Ivrita(); 231 | 232 | ivrita.setMode(FEMALE); 233 | expect(paragraph.innerHTML).toBe('מתכנתות רבות חשות תסכול, כאשר פונות אליהן שלא בשפתן.'); 234 | 235 | ivrita.setMode(MALE); 236 | expect(paragraph.innerHTML).toBe('מעצבים רבים מרגישים תסכול, כאשר פונים אליהם שלא בשפתם.'); 237 | 238 | ivrita.setMode(NEUTRAL); 239 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.'); 240 | }); 241 | 242 | test('data-ivrita-neutral', () => { 243 | document.body.innerHTML = '

[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.

'; 244 | const paragraph = document.querySelector('p'); 245 | 246 | const ivrita = new Ivrita(); 247 | 248 | ivrita.setMode(FEMALE); 249 | expect(paragraph.innerHTML).toBe('מתכנתות רבות מרגישות תסכול, כאשר פונות אליהן שלא בשפתן.'); 250 | 251 | ivrita.setMode(MALE); 252 | expect(paragraph.innerHTML).toBe('מעצבים רבים מרגישים תסכול, כאשר פונים אליהם שלא בשפתם.'); 253 | 254 | ivrita.setMode(NEUTRAL); 255 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות חשותים תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.'); 256 | }); 257 | 258 | test('No breaking space is preserved', () => { 259 | document.body.innerHTML = '

מתכנתים/ות רבים/ות

'; 260 | 261 | new Ivrita(document.body.childNodes[0], FEMALE); 262 | 263 | expect(document.body.innerHTML).toBe('

מתכנתות רבות

'); 264 | }); 265 | 266 | test('On-Instance genderize string', () => { 267 | const iv = new Ivrita(document.body); 268 | 269 | iv.setMode(FEMALE); 270 | expect(iv.genderize('איש/ה')).toBe('אישה'); 271 | 272 | iv.setMode(MALE); 273 | expect(iv.genderize('איש/ה')).toBe('איש'); 274 | }); 275 | -------------------------------------------------------------------------------- /test/rules/endings.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | genderize, FEMALE, MALE, NEUTRAL, 3 | } from '../../src/ivrita'; 4 | 5 | test('Word endings', () => { 6 | // Commandings 7 | expect(genderize('הקשב/י', FEMALE)).toBe('הקשיבי'); 8 | expect(genderize('הקשב/י', MALE)).toBe('הקשב'); 9 | expect(genderize('הקשב/י/ו', NEUTRAL)).toBe('הקשיבו'); 10 | 11 | expect(genderize('הפעל/י', FEMALE)).toBe('הפעילי'); 12 | expect(genderize('הפעל/י', MALE)).toBe('הפעל'); 13 | expect(genderize('הפעל/י/ו', NEUTRAL)).toBe('הפעילו'); 14 | 15 | expect(genderize('השב/י', FEMALE)).toBe('השיבי'); 16 | expect(genderize('השב/י', MALE)).toBe('השב'); 17 | expect(genderize('השב/י/ו', NEUTRAL)).toBe('השיבו'); 18 | 19 | expect(genderize('הרם/י', FEMALE)).toBe('הרימי'); 20 | expect(genderize('הרם/י', MALE)).toBe('הרם'); 21 | expect(genderize('הרם/י/ו', NEUTRAL)).toBe('הרימו'); 22 | expect(genderize('הרם/י/ו', FEMALE)).toBe('הרימי'); 23 | expect(genderize('הרם/י/ו', MALE)).toBe('הרם'); 24 | 25 | expect(genderize('הרמ/י', FEMALE)).toBe('הרימי'); 26 | expect(genderize('הרמ/י', MALE)).toBe('הרם'); 27 | expect(genderize('הרמ/י/ו', NEUTRAL)).toBe('הרימו'); 28 | expect(genderize('הרמ/י/ו', FEMALE)).toBe('הרימי'); 29 | expect(genderize('הרמ/י/ו', MALE)).toBe('הרם'); 30 | 31 | expect(genderize('הדגם/י', FEMALE)).toBe('הדגימי'); 32 | expect(genderize('הדגם/י', MALE)).toBe('הדגם'); 33 | expect(genderize('הדגם/י/ו', NEUTRAL)).toBe('הדגימו'); 34 | 35 | expect(genderize('הקלד/י', FEMALE)).toBe('הקלידי'); 36 | expect(genderize('הקלד/י', MALE)).toBe('הקלד'); 37 | expect(genderize('הקלד/י/ו', NEUTRAL)).toBe('הקלידו'); 38 | 39 | expect(genderize('הלבש/י', FEMALE)).toBe('הלבישי'); 40 | expect(genderize('הלבש/י', MALE)).toBe('הלבש'); 41 | expect(genderize('הלבש/י/ו', NEUTRAL)).toBe('הלבישו'); 42 | 43 | expect(genderize('הקש/י', FEMALE)).toBe('הקישי'); 44 | expect(genderize('הקש/י', MALE)).toBe('הקש'); 45 | expect(genderize('הקש/י/ו', NEUTRAL)).toBe('הקישו'); 46 | 47 | expect(genderize('צפה/י', FEMALE)).toBe('צפי'); 48 | expect(genderize('צפה/י', MALE)).toBe('צפה'); 49 | expect(genderize('צפה/י/ו', NEUTRAL)).toBe('צפו'); 50 | 51 | expect(genderize('שלח/י', FEMALE)).toBe('שלחי'); 52 | expect(genderize('שלח/י', MALE)).toBe('שלח'); 53 | expect(genderize('שלח/י/ו', FEMALE)).toBe('שלחי'); 54 | expect(genderize('שלח/י/ו', MALE)).toBe('שלח'); 55 | expect(genderize('שלח/י/ו', NEUTRAL)).toBe('שלחו'); 56 | 57 | expect(genderize('כתוב/י', FEMALE)).toBe('כתבי'); 58 | expect(genderize('כתוב/י', MALE)).toBe('כתוב'); 59 | expect(genderize('כתוב/י/ו', NEUTRAL)).toBe('כתבו'); 60 | 61 | expect(genderize('צור/י', FEMALE)).toBe('צרי'); 62 | expect(genderize('צור/י', MALE)).toBe('צור'); 63 | expect(genderize('צור/י/ו', NEUTRAL)).toBe('צרו'); 64 | 65 | expect(genderize('חשוב/י', FEMALE)).toBe('חשבי'); 66 | expect(genderize('חשוב/י', MALE)).toBe('חשוב'); 67 | expect(genderize('חשוב/י/ו', NEUTRAL)).toBe('חשבו'); 68 | 69 | expect(genderize('רוצ/י', FEMALE)).toBe('רוצי'); 70 | expect(genderize('רוצ/י', MALE)).toBe('רוץ'); 71 | expect(genderize('רוצ/י/ו', NEUTRAL)).toBe('רוצו'); 72 | 73 | expect(genderize('קומ/י', FEMALE)).toBe('קומי'); 74 | expect(genderize('קומ/י', MALE)).toBe('קום'); 75 | expect(genderize('קומ/י/ו', NEUTRAL)).toBe('קומו'); 76 | expect(genderize('לכשתקומ/י', FEMALE)).toBe('לכשתקומי'); 77 | expect(genderize('לכשתקומ/י', MALE)).toBe('לכשתקום'); 78 | expect(genderize('לכשתקומ/י/ו', NEUTRAL)).toBe('לכשתקומו'); 79 | 80 | expect(genderize('עופ/י', FEMALE)).toBe('עופי'); 81 | expect(genderize('עופ/י', MALE)).toBe('עוף'); 82 | expect(genderize('עופ/י/ו', NEUTRAL)).toBe('עופו'); 83 | 84 | expect(genderize('שים/י', FEMALE)).toBe('שימי'); 85 | expect(genderize('שים/י', MALE)).toBe('שים'); 86 | expect(genderize('שים/י/ו', NEUTRAL)).toBe('שימו'); 87 | 88 | expect(genderize('עקוב/י', FEMALE)).toBe('עקבי'); 89 | expect(genderize('עקוב/י', MALE)).toBe('עקוב'); 90 | expect(genderize('עקוב/י/ו', NEUTRAL)).toBe('עקבו'); 91 | 92 | expect(genderize('ראה/י', FEMALE)).toBe('ראי'); 93 | expect(genderize('ראה/י', MALE)).toBe('ראה'); 94 | expect(genderize('ראה/י/ו', NEUTRAL)).toBe('ראו'); 95 | 96 | expect(genderize('ודא/י', FEMALE)).toBe('ודאי'); 97 | expect(genderize('ודא/י', MALE)).toBe('ודא'); 98 | expect(genderize('ודא/י/ו', NEUTRAL)).toBe('ודאו'); 99 | 100 | expect(genderize('בחר/י', FEMALE)).toBe('בחרי'); 101 | expect(genderize('בחר/י', MALE)).toBe('בחר'); 102 | expect(genderize('בחר/י/ו', NEUTRAL)).toBe('בחרו'); 103 | 104 | expect(genderize('תוכל/י', FEMALE)).toBe('תוכלי'); 105 | expect(genderize('תוכל/י', MALE)).toBe('תוכל'); 106 | expect(genderize('תוכל/י/ו', NEUTRAL)).toBe('תוכלו'); 107 | 108 | expect(genderize('דווח/י', FEMALE)).toBe('דווחי'); 109 | expect(genderize('דווח/י', MALE)).toBe('דווח'); 110 | 111 | expect(genderize('כוון/י', FEMALE)).toBe('כווני'); 112 | expect(genderize('כוון/י', MALE)).toBe('כוון'); 113 | 114 | expect(genderize('תכוונ/י', FEMALE)).toBe('תכווני'); 115 | expect(genderize('תכוונ/י', MALE)).toBe('תכוון'); 116 | 117 | expect(genderize('צאו/נה', FEMALE)).toBe('צאנה'); 118 | expect(genderize('צאו/נה', MALE)).toBe('צאו'); 119 | 120 | expect(genderize('צאו/תצאנה', FEMALE)).toBe('תצאנה'); 121 | expect(genderize('צאו/תצאנה', MALE)).toBe('צאו'); 122 | 123 | expect(genderize('יספרו/תספרנה', FEMALE)).toBe('תספרנה'); 124 | expect(genderize('יספרו/תספרנה', MALE)).toBe('יספרו'); 125 | 126 | expect(genderize('תלכו/נה', FEMALE)).toBe('תלכנה'); 127 | expect(genderize('תלכו/נה', MALE)).toBe('תלכו'); 128 | 129 | expect(genderize('תדרכו/נה', FEMALE)).toBe('תדרכנה'); 130 | expect(genderize('תדרכו/נה', MALE)).toBe('תדרכו'); 131 | 132 | expect(genderize('הלבישו/נה', FEMALE)).toBe('הלבשנה'); 133 | expect(genderize('הלבישו/נה', MALE)).toBe('הלבישו'); 134 | 135 | expect(genderize('החזיקו/נה', FEMALE)).toBe('החזקנה'); 136 | expect(genderize('החזיקו/נה', MALE)).toBe('החזיקו'); 137 | 138 | expect(genderize('הביאו/נה', FEMALE)).toBe('הבאנה'); 139 | expect(genderize('הביאו/נה', MALE)).toBe('הביאו'); 140 | 141 | expect(genderize('החטיפו/נה', FEMALE)).toBe('החטפנה'); 142 | expect(genderize('החטיפו/נה', MALE)).toBe('החטיפו'); 143 | 144 | expect(genderize('הניחו/נה', FEMALE)).toBe('הנחנה'); 145 | expect(genderize('הניחו/נה', MALE)).toBe('הניחו'); 146 | 147 | expect(genderize('השתיקו/נה', FEMALE)).toBe('השתקנה'); 148 | expect(genderize('השתיקו/נה', MALE)).toBe('השתיקו'); 149 | 150 | expect(genderize('העיפו/נה', FEMALE)).toBe('העפנה'); 151 | expect(genderize('העיפו/נה', MALE)).toBe('העיפו'); 152 | 153 | expect(genderize('הניחו/נה', FEMALE)).toBe('הנחנה'); 154 | expect(genderize('הניחו/נה', MALE)).toBe('הניחו'); 155 | 156 | expect(genderize('רקדו/נה', FEMALE)).toBe('רקדנה'); 157 | expect(genderize('רקדו/נה', MALE)).toBe('רקדו'); 158 | 159 | // Third person 160 | expect(genderize('לו/ה', FEMALE)).toBe('לה'); 161 | expect(genderize('לו/ה', MALE)).toBe('לו'); 162 | expect(genderize('לה/ו', FEMALE)).toBe('לה'); 163 | expect(genderize('לה/ו', MALE)).toBe('לו'); 164 | expect(genderize('לו/לה', FEMALE)).toBe('לה'); 165 | expect(genderize('לו/לה', MALE)).toBe('לו'); 166 | expect(genderize('לה/לו', FEMALE)).toBe('לה'); 167 | expect(genderize('לה/לו', MALE)).toBe('לו'); 168 | 169 | expect(genderize('עשה/תה', FEMALE)).toBe('עשתה'); 170 | expect(genderize('עשה/תה', MALE)).toBe('עשה'); 171 | 172 | expect(genderize('בכה/תה', FEMALE)).toBe('בכתה'); 173 | expect(genderize('בכה/תה', MALE)).toBe('בכה'); 174 | 175 | expect(genderize('רצ/תה', FEMALE)).toBe('רצתה'); 176 | expect(genderize('רצ/תה', MALE)).toBe('רצה'); 177 | 178 | expect(genderize('כפ/תה', FEMALE)).toBe('כפתה'); 179 | expect(genderize('כפ/תה', MALE)).toBe('כפה'); 180 | 181 | expect(genderize('הלווה/תה', FEMALE)).toBe('הלוותה'); 182 | expect(genderize('הלווה/תה', MALE)).toBe('הלווה'); 183 | 184 | expect(genderize('יקירי/תי', FEMALE)).toBe('יקירתי'); 185 | expect(genderize('יקירי/תי', MALE)).toBe('יקירי'); 186 | 187 | expect(genderize('אהובי/תי', FEMALE)).toBe('אהובתי'); 188 | expect(genderize('אהובי/תי', MALE)).toBe('אהובי'); 189 | 190 | expect(genderize('דודי/תי', FEMALE)).toBe('דודתי'); 191 | expect(genderize('דודי/תי', MALE)).toBe('דודי'); 192 | 193 | expect(genderize('שלו/ה', FEMALE)).toBe('שלה'); 194 | expect(genderize('שלו/ה', MALE)).toBe('שלו'); 195 | 196 | expect(genderize('מחקריו/ה', FEMALE)).toBe('מחקריה'); 197 | expect(genderize('מחקריו/ה', MALE)).toBe('מחקריו'); 198 | expect(genderize('מחקריו/יה', FEMALE)).toBe('מחקריה'); 199 | expect(genderize('מחקריו/יה', MALE)).toBe('מחקריו'); 200 | 201 | expect(genderize('מועמדותו/ה', FEMALE)).toBe('מועמדותה'); 202 | expect(genderize('מועמדותו/ה', MALE)).toBe('מועמדותו'); 203 | 204 | expect(genderize('מועמדותן/ם', FEMALE)).toBe('מועמדותן'); 205 | expect(genderize('מועמדותן/ם', MALE)).toBe('מועמדותם'); 206 | 207 | expect(genderize('שלכם/ן', FEMALE)).toBe('שלכן'); 208 | expect(genderize('שלכם/ן', MALE)).toBe('שלכם'); 209 | expect(genderize('שלכן/ם', FEMALE)).toBe('שלכן'); 210 | expect(genderize('שלכן/ם', MALE)).toBe('שלכם'); 211 | 212 | expect(genderize('מחקריהם/ן', FEMALE)).toBe('מחקריהן'); 213 | expect(genderize('מחקריהם/ן', MALE)).toBe('מחקריהם'); 214 | expect(genderize('מחקריהן/ם', FEMALE)).toBe('מחקריהן'); 215 | expect(genderize('מחקריהן/ם', MALE)).toBe('מחקריהם'); 216 | 217 | expect(genderize('בגללו/ה', FEMALE)).toBe('בגללה'); 218 | expect(genderize('בגללו/ה', MALE)).toBe('בגללו'); 219 | 220 | expect(genderize('מינו/ה', FEMALE)).toBe('מינה'); 221 | expect(genderize('מינו/ה', MALE)).toBe('מינו'); 222 | 223 | expect(genderize('לגביו/ה', FEMALE)).toBe('לגביה'); 224 | expect(genderize('לגביו/ה', MALE)).toBe('לגביו'); 225 | 226 | // Singular 227 | expect(genderize('איש/ה', FEMALE)).toBe('אישה'); 228 | expect(genderize('איש/ה', MALE)).toBe('איש'); 229 | expect(genderize('איש/ת', FEMALE)).toBe('אשת'); 230 | expect(genderize('איש/ת', MALE)).toBe('איש'); 231 | 232 | expect(genderize('חרוץ/ה', FEMALE)).toBe('חרוצה'); 233 | expect(genderize('חרוץ/ה', MALE)).toBe('חרוץ'); 234 | 235 | expect(genderize('חרוץ/צה', FEMALE)).toBe('חרוצה'); 236 | expect(genderize('חרוץ/צה', MALE)).toBe('חרוץ'); 237 | expect(genderize('מוכן/נה', FEMALE)).toBe('מוכנה'); 238 | expect(genderize('מוכן/נה', MALE)).toBe('מוכן'); 239 | 240 | expect(genderize('גבוה/ה', FEMALE)).toBe('גבוהה'); 241 | expect(genderize('גבוה/ה', MALE)).toBe('גבוה'); 242 | 243 | expect(genderize('סטודנט/ית', FEMALE)).toBe('סטודנטית'); 244 | expect(genderize('סטודנט/ית', MALE)).toBe('סטודנט'); 245 | 246 | expect(genderize('יזמ/ית', FEMALE)).toBe('יזמית'); 247 | expect(genderize('יזמ/ית', MALE)).toBe('יזם'); 248 | expect(genderize('יזם/ית', FEMALE)).toBe('יזמית'); 249 | expect(genderize('יזם/ית', MALE)).toBe('יזם'); 250 | expect(genderize('יזמ/ת', FEMALE)).toBe('יזמת'); 251 | expect(genderize('יזמ/ת', MALE)).toBe('יזם'); 252 | expect(genderize('יזם/ת', FEMALE)).toBe('יזמת'); 253 | expect(genderize('יזם/ת', MALE)).toBe('יזם'); 254 | 255 | expect(genderize('מומחה/ית', FEMALE)).toBe('מומחית'); 256 | expect(genderize('מומחה/ית', MALE)).toBe('מומחה'); 257 | 258 | expect(genderize('חוקר/ת', FEMALE)).toBe('חוקרת'); 259 | expect(genderize('חוקר/ת', MALE)).toBe('חוקר'); 260 | 261 | expect(genderize('חבר/ה', FEMALE)).toBe('חברה'); 262 | expect(genderize('חבר/ה', MALE)).toBe('חבר'); 263 | 264 | expect(genderize('חבר/ת סגל', FEMALE)).toBe('חברת סגל'); 265 | expect(genderize('חבר/ת סגל', MALE)).toBe('חבר סגל'); 266 | 267 | expect(genderize('רואה/ת חשבון', FEMALE)).toBe('רואת חשבון'); 268 | expect(genderize('רואה/ת חשבון', MALE)).toBe('רואה חשבון'); 269 | 270 | expect(genderize('מנקה/ת בתים', FEMALE)).toBe('מנקת בתים'); 271 | expect(genderize('מנקה/ת בתים', MALE)).toBe('מנקה בתים'); 272 | 273 | expect(genderize('מורה פרטי/ת', FEMALE)).toBe('מורה פרטית'); 274 | expect(genderize('מורה פרטי/ת', MALE)).toBe('מורה פרטי'); 275 | 276 | expect(genderize('עוזר/ת אישי/ת', FEMALE)).toBe('עוזרת אישית'); 277 | expect(genderize('עוזר/ת אישי/ת', MALE)).toBe('עוזר אישי'); 278 | 279 | expect(genderize('מכונאי/ת', FEMALE)).toBe('מכונאית'); 280 | expect(genderize('מכונאי/ת', MALE)).toBe('מכונאי'); 281 | 282 | expect(genderize('שומר/ת', FEMALE)).toBe('שומרת'); 283 | expect(genderize('שומר/ת', MALE)).toBe('שומר'); 284 | 285 | expect(genderize('צלמ/ת', FEMALE)).toBe('צלמת'); 286 | expect(genderize('צלמ/ת', MALE)).toBe('צלם'); 287 | expect(genderize('צלם/ת', FEMALE)).toBe('צלמת'); 288 | expect(genderize('צלם/ת', MALE)).toBe('צלם'); 289 | 290 | expect(genderize('ישראלי/ת', FEMALE)).toBe('ישראלית'); 291 | expect(genderize('ישראלי/ת', MALE)).toBe('ישראלי'); 292 | 293 | expect(genderize('ארגנטינאי/ת', FEMALE)).toBe('ארגנטינאית'); 294 | expect(genderize('ארגנטינאי/ת', MALE)).toBe('ארגנטינאי'); 295 | 296 | expect(genderize('צרפתי/ת', FEMALE)).toBe('צרפתית'); 297 | expect(genderize('צרפתי/ת', MALE)).toBe('צרפתי'); 298 | 299 | expect(genderize('ברברי/ת', FEMALE)).toBe('ברברית'); 300 | expect(genderize('ברברי/ת', MALE)).toBe('ברברי'); 301 | expect(genderize('ברבארי/ת', FEMALE)).toBe('ברבארית'); 302 | expect(genderize('ברבארי/ת', MALE)).toBe('ברבארי'); 303 | 304 | expect(genderize('יהודי/ת', FEMALE)).toBe('יהודית'); 305 | expect(genderize('יהודי/ת', MALE)).toBe('יהודי'); 306 | expect(genderize('יהודי/ה', FEMALE)).toBe('יהודיה'); 307 | expect(genderize('יהודי/ה', MALE)).toBe('יהודי'); 308 | 309 | // Plural 310 | expect(genderize('חיים/ות', FEMALE)).toBe('חיות'); 311 | expect(genderize('חיים/ות', MALE)).toBe('חיים'); 312 | 313 | expect(genderize('טריים/ות', FEMALE)).toBe('טריות'); 314 | expect(genderize('טריים/ות', MALE)).toBe('טריים'); 315 | 316 | expect(genderize('פנויים/ות', FEMALE)).toBe('פנויות'); 317 | expect(genderize('פנויים/ות', MALE)).toBe('פנויים'); 318 | 319 | expect(genderize('ערביים/ות', FEMALE)).toBe('ערביות'); 320 | expect(genderize('ערביים/ות', MALE)).toBe('ערביים'); 321 | 322 | expect(genderize('סטודנטים/ות', FEMALE)).toBe('סטודנטיות'); 323 | expect(genderize('סטודנטים/ות', MALE)).toBe('סטודנטים'); 324 | expect(genderize('סטודנטים/יות', FEMALE)).toBe('סטודנטיות'); 325 | expect(genderize('סטודנטים/יות', MALE)).toBe('סטודנטים'); 326 | 327 | expect(genderize('מאסטרים/ות', FEMALE)).toBe('מאסטריות'); 328 | expect(genderize('מאסטרים/ות', MALE)).toBe('מאסטרים'); 329 | 330 | expect(genderize('יזמים/ות', FEMALE)).toBe('יזמיות'); 331 | expect(genderize('יזמים/ות', MALE)).toBe('יזמים'); 332 | 333 | expect(genderize('מומחים/ות', FEMALE)).toBe('מומחיות'); 334 | expect(genderize('מומחים/ות', MALE)).toBe('מומחים'); 335 | 336 | expect(genderize('עורכים/ות', FEMALE)).toBe('עורכות'); 337 | expect(genderize('עורכים/ות', MALE)).toBe('עורכים'); 338 | 339 | expect(genderize('שופטים/ות', FEMALE)).toBe('שופטות'); 340 | expect(genderize('שופטים/ות', MALE)).toBe('שופטים'); 341 | 342 | expect(genderize('בעלות/י', FEMALE)).toBe('בעלות'); 343 | expect(genderize('בעלות/י', MALE)).toBe('בעלי'); 344 | expect(genderize('בעלי/ות', FEMALE)).toBe('בעלות'); 345 | expect(genderize('בעלי/ות', MALE)).toBe('בעלי'); 346 | expect(genderize('בעלים/ות', FEMALE)).toBe('בעלות'); 347 | expect(genderize('בעלים/ות', MALE)).toBe('בעלים'); 348 | 349 | expect(genderize('מעצבי/ות־על', FEMALE)).toBe('מעצבות־על'); 350 | expect(genderize('מעצבי/ות־על', MALE)).toBe('מעצבי־על'); 351 | 352 | expect(genderize('רואי/ות חשבון', FEMALE)).toBe('רואות חשבון'); 353 | expect(genderize('רואי/ות חשבון', MALE)).toBe('רואי חשבון'); 354 | 355 | expect(genderize('מנקי/ות בתים', FEMALE)).toBe('מנקות בתים'); 356 | expect(genderize('מנקי/ות בתים', MALE)).toBe('מנקי בתים'); 357 | 358 | expect(genderize('מורים/ות פרטיים/ות', FEMALE)).toBe('מורות פרטיות'); 359 | expect(genderize('מורים/ות פרטיים/ות', MALE)).toBe('מורים פרטיים'); 360 | 361 | expect(genderize('עוזרים/ות אישיים/ות', FEMALE)).toBe('עוזרות אישיות'); 362 | expect(genderize('עוזרים/ות אישיים/ות', MALE)).toBe('עוזרים אישיים'); 363 | 364 | expect(genderize('עוזרות/ים אישיות/ים', FEMALE)).toBe('עוזרות אישיות'); 365 | expect(genderize('עוזרות/ים אישיות/ים', MALE)).toBe('עוזרים אישיים'); 366 | 367 | expect(genderize('מכונאים/ות', FEMALE)).toBe('מכונאיות'); 368 | expect(genderize('מכונאים/ות', MALE)).toBe('מכונאים'); 369 | 370 | expect(genderize('שומרים/ות', FEMALE)).toBe('שומרות'); 371 | expect(genderize('שומרים/ות', MALE)).toBe('שומרים'); 372 | 373 | expect(genderize('צלמים/ות', FEMALE)).toBe('צלמות'); 374 | expect(genderize('צלמים/ות', MALE)).toBe('צלמים'); 375 | expect(genderize('צלמים/יות', FEMALE)).toBe('צלמיות'); 376 | expect(genderize('צלמים/יות', MALE)).toBe('צלמים'); 377 | 378 | expect(genderize('ישראלים/ות', FEMALE)).toBe('ישראליות'); 379 | expect(genderize('ישראלים/ות', MALE)).toBe('ישראלים'); 380 | expect(genderize('ישראליים/ות', FEMALE)).toBe('ישראליות'); 381 | expect(genderize('ישראליים/ות', MALE)).toBe('ישראליים'); 382 | 383 | expect(genderize('ברברים/ות', FEMALE)).toBe('ברבריות'); 384 | expect(genderize('ברברים/ות', MALE)).toBe('ברברים'); 385 | expect(genderize('ברבארים/ות', FEMALE)).toBe('ברבאריות'); 386 | expect(genderize('ברבארים/ות', MALE)).toBe('ברבארים'); 387 | 388 | expect(genderize('סחים/ות', FEMALE)).toBe('סחיות'); 389 | expect(genderize('סחים/ות', MALE)).toBe('סחים'); 390 | expect(genderize('סאחים/ות', FEMALE)).toBe('סאחיות'); 391 | expect(genderize('סאחים/ות', MALE)).toBe('סאחים'); 392 | 393 | expect(genderize('ח"כים/ות', FEMALE)).toBe('ח"כיות'); 394 | expect(genderize('ח"כים/ות', MALE)).toBe('ח"כים'); 395 | 396 | expect(genderize('משת”פים/ות', FEMALE)).toBe('משת”פיות'); 397 | expect(genderize('משת”פים/ות', MALE)).toBe('משת”פים'); 398 | 399 | expect(genderize('כשהגננים/ות', FEMALE)).toBe('כשהגננות'); 400 | expect(genderize('כשהגננים/ות', MALE)).toBe('כשהגננים'); 401 | 402 | expect(genderize('תל אביבים/ות', FEMALE)).toBe('תל אביביות'); 403 | expect(genderize('תל אביבים/ות', MALE)).toBe('תל אביבים'); 404 | expect(genderize('תל־אביבים/ות', FEMALE)).toBe('תל־אביביות'); 405 | expect(genderize('תל־אביבים/ות', MALE)).toBe('תל־אביבים'); 406 | 407 | expect(genderize('ברברים/ות', FEMALE)).toBe('ברבריות'); 408 | expect(genderize('ברברים/ות', MALE)).toBe('ברברים'); 409 | expect(genderize('ברבארים/ות', FEMALE)).toBe('ברבאריות'); 410 | expect(genderize('ברבארים/ות', MALE)).toBe('ברבארים'); 411 | 412 | expect(genderize('סחים/ות', FEMALE)).toBe('סחיות'); 413 | expect(genderize('סחים/ות', MALE)).toBe('סחים'); 414 | expect(genderize('סאחים/ות', FEMALE)).toBe('סאחיות'); 415 | expect(genderize('סאחים/ות', MALE)).toBe('סאחים'); 416 | 417 | expect(genderize('ח"כים/ות', FEMALE)).toBe('ח"כיות'); 418 | expect(genderize('ח"כים/ות', MALE)).toBe('ח"כים'); 419 | 420 | expect(genderize('משת”פים/ות', FEMALE)).toBe('משת”פיות'); 421 | expect(genderize('משת”פים/ות', MALE)).toBe('משת”פים'); 422 | 423 | expect(genderize('כשהגננים/ות', FEMALE)).toBe('כשהגננות'); 424 | expect(genderize('כשהגננים/ות', MALE)).toBe('כשהגננים'); 425 | 426 | expect(genderize('ארגנטינאים/ות', FEMALE)).toBe('ארגנטינאיות'); 427 | expect(genderize('ארגנטינאים/ות', MALE)).toBe('ארגנטינאים'); 428 | 429 | expect(genderize('צרפתים/ות', FEMALE)).toBe('צרפתיות'); 430 | expect(genderize('צרפתים/ות', MALE)).toBe('צרפתים'); 431 | 432 | expect(genderize('אחיינים/ות', FEMALE)).toBe('אחייניות'); 433 | expect(genderize('אחיינים/ות', MALE)).toBe('אחיינים'); 434 | 435 | expect(genderize('הודים/ות', FEMALE)).toBe('הודיות'); 436 | expect(genderize('הודים/ות', MALE)).toBe('הודים'); 437 | expect(genderize('יהודים/ות', FEMALE)).toBe('יהודיות'); 438 | expect(genderize('יהודים/ות', MALE)).toBe('יהודים'); 439 | expect(genderize('יהודיים/ות', FEMALE)).toBe('יהודיות'); 440 | expect(genderize('יהודיים/ות', MALE)).toBe('יהודיים'); 441 | 442 | expect(genderize('קלפטומנים/ות', FEMALE)).toBe('קלפטומניות'); 443 | expect(genderize('קלפטומנים/ות', MALE)).toBe('קלפטומנים'); 444 | expect(genderize('קלפטומנים/יות', FEMALE)).toBe('קלפטומניות'); 445 | expect(genderize('קלפטומנים/יות', MALE)).toBe('קלפטומנים'); 446 | 447 | expect(genderize('שפים/ות', FEMALE)).toBe('שפיות'); 448 | expect(genderize('שפים/ות', MALE)).toBe('שפים'); 449 | expect(genderize('מכשפים/ות', FEMALE)).toBe('מכשפות'); 450 | expect(genderize('מכשפים/ות', MALE)).toBe('מכשפים'); 451 | 452 | expect(genderize('שווים/ות, שוויםות, שוות/ים, שווי/ות זכויות', FEMALE)).toBe('שוות, שוות, שוות, שוות זכויות'); 453 | expect(genderize('שווים/ות, שוויםות, שוות/ים, שווי/ות זכויות', MALE)).toBe('שווים, שווים, שווים, שווי זכויות'); 454 | }); 455 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------