├── .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 |
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 | 
2 | 
3 |
4 |
5 |
6 |
7 |
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 |
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 |
14 |
[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.
15 |
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('מתכנתות רבות מרגישות תסכול, כאשר פונות אליהן שלא בשפתן.');
27 |
28 | // Male
29 | ivrita.setMode(MALE);
30 | expect(paragraph.innerHTML).toBe('מעצבים רבים מרגישים תסכול, כאשר פונים אליהם שלא בשפתם.');
31 |
32 | // Neutral
33 | ivrita.setMode(NEUTRAL);
34 | expect(paragraph.innerHTML).toBe('הייטקיסטים רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.');
35 |
36 | // Back to original
37 | ivrita.setMode(ORIGINAL);
38 | expect(paragraph.innerHTML).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.');
39 |
40 | // Back to male
41 | ivrita.setMode(MALE);
42 | expect(paragraph.innerHTML).toBe('מעצבים רבים מרגישים תסכול, כאשר פונים אליהם שלא בשפתם.');
43 |
44 | // Destroy
45 | ivrita.destroy();
46 | expect(paragraph.innerHTML).toBe('[מעצבים|מתכנתות|הייטקיסטים] רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.');
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('הייטקיסטים רבים/ות מרגישים/ות תסכול, כאשר פונים/ות אליהם/ן שלא בשפתם/ן.');
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(' tag is changed', () => {
96 | document.documentElement.innerHTML = `
97 |
98 |
99 | צור/י קשר
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 |
--------------------------------------------------------------------------------