├── .github ├── CODEOWNERS ├── pull_request_template.md ├── workflows │ ├── build.yml │ ├── publish.yml │ └── tests.yml └── actions │ └── launch │ └── action.yml ├── .gitignore ├── backend ├── hyperglosae │ ├── src │ │ ├── _id │ │ ├── views │ │ │ ├── types │ │ │ │ └── map.js │ │ │ ├── bookmark │ │ │ │ └── map.js │ │ │ ├── history │ │ │ │ └── map.js │ │ │ ├── content │ │ │ │ └── map.js │ │ │ ├── all_documents │ │ │ │ └── map.js │ │ │ ├── metadata │ │ │ │ └── map.js │ │ │ └── lib │ │ │ │ └── links.js │ │ ├── validate_doc_update.js │ │ └── updates │ │ │ └── document.js │ └── _security.json ├── _config.json └── _users │ └── _security.json ├── frontend ├── src │ ├── styles │ │ ├── Menu.css │ │ ├── Passage.css │ │ ├── CroppedImage.css │ │ ├── VideoComment.css │ │ ├── EditableText.css │ │ ├── FragmentComment.css │ │ ├── DiscreeteDropdown.css │ │ ├── Bookshelf.css │ │ ├── Metadata.css │ │ ├── index.css │ │ ├── Type.css │ │ ├── FutureDocument.css │ │ ├── HistoryInfo.css │ │ └── Lectern.css │ ├── components │ │ ├── TypesContext.js │ │ ├── DiscreeteDropdown.jsx │ │ ├── License.jsx │ │ ├── DocumentNotFound.jsx │ │ ├── CroppedImage.jsx │ │ ├── FragmentComment.jsx │ │ ├── DocumentsCards.jsx │ │ ├── VideoComment.jsx │ │ ├── LicenseCompatibility.jsx │ │ ├── DocumentList.jsx │ │ ├── ExistingDocument.jsx │ │ ├── BrowseTools.jsx │ │ ├── Bookmark.jsx │ │ ├── FormattedText.jsx │ │ ├── CheckboxList.jsx │ │ ├── Menu.jsx │ │ ├── Metadata.jsx │ │ ├── Type.jsx │ │ ├── Passage.jsx │ │ ├── EditableText.jsx │ │ └── Graph.jsx │ ├── main.jsx │ ├── menu-items │ │ ├── SignOutAction.jsx │ │ ├── EditRawDocumentAction.jsx │ │ ├── CommentFragmentAction.jsx │ │ ├── PictureUploadAction.jsx │ │ ├── DeleteDocumentAction.jsx │ │ ├── BreakIntoPassagesAction.jsx │ │ ├── DeleteReferenceToDocumentAction.jsx │ │ └── InviteEditorsAction.jsx │ ├── context.js │ ├── App.jsx │ ├── routes │ │ ├── Registration.jsx │ │ ├── Bookshelf.jsx │ │ └── Lectern.jsx │ ├── hyperglosae.js │ ├── README.md │ └── parallelDocuments.js ├── public │ ├── logo.png │ ├── robots.txt │ ├── favicon.ico │ └── manifest.json ├── scenarios │ ├── add_picture.feature │ ├── bookmark_document.feature │ ├── include_video.feature │ ├── comment_fragment.feature │ ├── focus_on_document.feature │ ├── show_fragment.feature │ ├── set_type.feature │ ├── link_document.feature │ ├── edit_passage.feature │ ├── open_uri.feature │ ├── edit_metadata.feature │ ├── set_license.feature │ ├── invite_editor.feature │ ├── show_history.feature │ ├── delete_reference.feature │ ├── break_into_passages.feature │ ├── edit_content.feature │ ├── open_document.feature │ └── create_document.feature ├── index.html ├── .gitignore ├── cypress.config.js ├── vite.config.js ├── package.json ├── eslint.config.js └── tests │ └── support.js ├── docs ├── architecture.png ├── component_graph.png ├── component_lectern.png ├── component_lectern2.png ├── component_metadata.png ├── component_passage.png ├── component_passage2.png ├── component_passage3.png ├── component_bookshelf.png ├── component_browsetools.png ├── component_metadata2.png ├── component_browsetools2.png ├── component_documentscards.png ├── component_documentscards2.png ├── component_futuredocument.png ├── component_futuredocument2.png ├── component_futuredocument3.png ├── component_openeddocuments.png ├── component_openeddocuments2.png ├── screenshot_analyst_parallel.png ├── screenshot_analyst_text_whole.png ├── screenshot_translator_parallel.png ├── screenshot_analyst_picture_whole.png ├── screenshot_translator_forwardlink.png ├── screenshot_translatior_reverselinks.png ├── screenshot_historian_transclusion_links.png └── screenshot_historian_transclusion_reverselinks.png ├── samples ├── _users │ ├── bill.json │ ├── alice.json │ └── christophe.json └── hyperglosae │ ├── ethnography_type01.json │ ├── ethnography_type02.json │ ├── ethnography_type03.json │ ├── cinema_video.json │ ├── imaj_2018_A.json │ ├── imaj_2018_D.json │ ├── imaj_2018_B.json │ ├── imaj_2018_C.json │ ├── imaj_2019_C.json │ ├── imaj_2019_B.json │ ├── imaj_2019_D.json │ ├── imaj_2019_A.json │ ├── perrault_lorinszky09.json │ ├── perrault_jamborova09.json │ ├── perrault_glossaire.json │ ├── revelation_snz113.json │ ├── perrault_lorinszky10.json │ ├── revelation_grv005_85.json │ ├── cinema_appreciation.json │ ├── revelation_smv006_12.json │ ├── revelation_smv006_7f.json │ ├── perrault_jamborova10.json │ ├── inrap_E.json │ ├── perrault_glossaire_content.json │ ├── perrault_lorinszky03.json │ ├── perrault_1697.json │ ├── perrault_jamborova03.json │ ├── ethnography03.json │ ├── perrault_jamborova02.json │ ├── cinema_note_rire_El2.json │ ├── imaj_expo_enfants.json │ ├── inrap_C.json │ ├── perrault_lorinszky02.json │ ├── cinema_mimes.json │ ├── perrault_1886.json │ ├── ethnography02.json │ ├── perrault_bintizulkiflee.json │ ├── perrault_lorinszky.json │ ├── imaj_expo_2018.json │ ├── imaj_expo_2019.json │ ├── perrault_jamborova04.json │ ├── perrault_lorinszky04.json │ ├── revelation_washing.json │ ├── ethnography01.json │ ├── perrault_jamborova.json │ ├── inrap_E_content03.json │ ├── cinema_note_rire_El1.json │ ├── inrap_E_content02.json │ ├── revelation_stars.json │ ├── perrault_lorinszky01.json │ ├── perrault_lorinszky05.json │ ├── perrault_lorinszky08.json │ ├── inrap_A.json │ ├── perrault_jamborova05.json │ ├── perrault_jamborova08.json │ ├── perrault_jamborova01.json │ ├── ethnography05.json │ ├── perrault_lorinszky06.json │ ├── inrap_B.json │ ├── perrault_jamborova06.json │ ├── inrap_E_content01.json │ ├── perrault_jamborova07.json │ ├── perrault_lorinszky07.json │ ├── inrap_D.json │ ├── cinema_script.json │ ├── ethnography04.json │ ├── perrault_1886_content.json │ └── perrault_1697_content.json ├── Dockerfile ├── settings ├── nginx.conf └── haproxy.cfg ├── requirements ├── .greenframe.yml ├── impact_of_bookshelf.js └── impact_of_parallel_texts.js ├── docker-compose.yml ├── docker-compose.dev.yml ├── docker-compose.test.yml ├── tools └── from_cassandre.js └── CONTRIBUTING.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @benel 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | screenshots 3 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/_id: -------------------------------------------------------------------------------- 1 | _design/app 2 | -------------------------------------------------------------------------------- /frontend/src/styles/Menu.css: -------------------------------------------------------------------------------- 1 | .navbar a { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /backend/_config.json: -------------------------------------------------------------------------------- 1 | {"couchdb":{"users_db_security_editable": "true"}} 2 | -------------------------------------------------------------------------------- /frontend/src/styles/Passage.css: -------------------------------------------------------------------------------- 1 | mark { 2 | padding: 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /backend/_users/_security.json: -------------------------------------------------------------------------------- 1 | {"members":{"roles":[]},"admins":{"roles":["_admin"]}} 2 | -------------------------------------------------------------------------------- /backend/hyperglosae/_security.json: -------------------------------------------------------------------------------- 1 | {"members":{"roles":[]},"admins":{"roles":["_admin"]}} 2 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /docs/component_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_graph.png -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/component_lectern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_lectern.png -------------------------------------------------------------------------------- /docs/component_lectern2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_lectern2.png -------------------------------------------------------------------------------- /docs/component_metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_metadata.png -------------------------------------------------------------------------------- /docs/component_passage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_passage.png -------------------------------------------------------------------------------- /docs/component_passage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_passage2.png -------------------------------------------------------------------------------- /docs/component_passage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_passage3.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/styles/CroppedImage.css: -------------------------------------------------------------------------------- 1 | .figure { 2 | margin-bottom: 15px; 3 | margin-top: 15px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /docs/component_bookshelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_bookshelf.png -------------------------------------------------------------------------------- /docs/component_browsetools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_browsetools.png -------------------------------------------------------------------------------- /docs/component_metadata2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_metadata2.png -------------------------------------------------------------------------------- /docs/component_browsetools2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_browsetools2.png -------------------------------------------------------------------------------- /docs/component_documentscards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_documentscards.png -------------------------------------------------------------------------------- /docs/component_documentscards2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_documentscards2.png -------------------------------------------------------------------------------- /docs/component_futuredocument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_futuredocument.png -------------------------------------------------------------------------------- /docs/component_futuredocument2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_futuredocument2.png -------------------------------------------------------------------------------- /docs/component_futuredocument3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_futuredocument3.png -------------------------------------------------------------------------------- /docs/component_openeddocuments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_openeddocuments.png -------------------------------------------------------------------------------- /docs/component_openeddocuments2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/component_openeddocuments2.png -------------------------------------------------------------------------------- /samples/_users/bill.json: -------------------------------------------------------------------------------- 1 | {"_id":"org.couchdb.user:bill", "name":"bill", "password":"madhatter", "roles":[], "type":"user"} 2 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/types/map.js: -------------------------------------------------------------------------------- 1 | function (metadata) { 2 | if(!metadata.type_name) return; 3 | emit(metadata._id); 4 | } -------------------------------------------------------------------------------- /docs/screenshot_analyst_parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_analyst_parallel.png -------------------------------------------------------------------------------- /samples/_users/alice.json: -------------------------------------------------------------------------------- 1 | {"_id":"org.couchdb.user:alice", "name":"alice", "password":"whiterabbit", "roles":[], "type":"user"} 2 | -------------------------------------------------------------------------------- /docs/screenshot_analyst_text_whole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_analyst_text_whole.png -------------------------------------------------------------------------------- /docs/screenshot_translator_parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_translator_parallel.png -------------------------------------------------------------------------------- /frontend/src/components/TypesContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const TypesContext = createContext([]); 4 | -------------------------------------------------------------------------------- /docs/screenshot_analyst_picture_whole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_analyst_picture_whole.png -------------------------------------------------------------------------------- /docs/screenshot_translator_forwardlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_translator_forwardlink.png -------------------------------------------------------------------------------- /samples/_users/christophe.json: -------------------------------------------------------------------------------- 1 | {"_id":"org.couchdb.user:christophe", "name":"christophe", "password":"redqueen", "roles":[], "type":"user"} 2 | -------------------------------------------------------------------------------- /docs/screenshot_translatior_reverselinks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_translatior_reverselinks.png -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/bookmark/map.js: -------------------------------------------------------------------------------- 1 | function (doc) { 2 | if (doc.bookmark) { 3 | emit([doc.editors[0], doc.bookmark]); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/screenshot_historian_transclusion_links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_historian_transclusion_links.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY ./settings/nginx.conf /etc/nginx/conf.d/default.conf 4 | COPY ./frontend/build /usr/share/nginx/html 5 | 6 | EXPOSE 80 7 | -------------------------------------------------------------------------------- /docs/screenshot_historian_transclusion_reverselinks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypertopic/HyperGlosae/HEAD/docs/screenshot_historian_transclusion_reverselinks.png -------------------------------------------------------------------------------- /settings/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location / { 3 | root /usr/share/nginx/html; 4 | try_files $uri /index.html; #Emulate multi-page app 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography_type01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "3547d291ea75f3f5a1832f1d9f0004ba", 3 | "type_name": "Ethnography/Interview", 4 | "color": "#0d6efd" 5 | } 6 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography_type02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "3547d291ea75f3f5a1832f1d9f0035ef", 3 | "type_name": "Ethnography/Report", 4 | "color": "#0dcaf0" 5 | } 6 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography_type03.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "3547d291ea75f3f5a1832f1d9f0045cf", 3 | "type_name": "Ethnography/Analysis", 4 | "color": "#ffc107" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App.jsx'; 3 | 4 | createRoot(document.getElementById('root')).render( 5 | 6 | ); 7 | -------------------------------------------------------------------------------- /frontend/src/styles/VideoComment.css: -------------------------------------------------------------------------------- 1 | .videoComment:hover{ 2 | color:blue; 3 | cursor: pointer; 4 | } 5 | 6 | .videoComment { 7 | margin-top: 15px; 8 | font-weight: bold; 9 | } -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/history/map.js: -------------------------------------------------------------------------------- 1 | function (doc) { 2 | if (doc.dc_title) { 3 | emit([doc._id], {history: doc.history}); 4 | } else if (doc.isPartOf) { 5 | emit([doc.isPartOf], {history : doc.history}) 6 | } 7 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | We, [FILL WITH FULL NAMES], hereby grant to Hyperglosae maintainers the right to publish our contribution under the terms of any licenses the Free Software Foundation classifies as Free Software Licenses. 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/styles/EditableText.css: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | padding: 0 12px; 4 | } 5 | 6 | .formatted-text { 7 | flex: 1; 8 | } 9 | 10 | .formatted-text:hover { 11 | background: lightgray; 12 | border-color: black; 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_video.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b009125", 3 | "dc_title": "Vidéo Sherlock Jr.", 4 | "dc_creator": "Buster Keaton", 5 | "dc_issued": "2023-02-21", 6 | "text": " ![](https://www.youtube.com/watch?v=JRXkAhMYKEc)" 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/styles/FragmentComment.css: -------------------------------------------------------------------------------- 1 | .fragment { 2 | padding-bottom: .6em; 3 | } 4 | 5 | .fragment:hover { 6 | cursor: pointer; 7 | } 8 | 9 | .fragment .citation { 10 | margin-right: 1.5em; 11 | background-color: rgb(255, 243, 205); 12 | font-weight: bold; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2018_A.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "8844c7de386e11eeb84fc79a7aaea501", 3 | "dc_title": "1er Prix - Catégorie 3-5 ans", 4 | "dc_spatial": "Canada", 5 | "dc_creator": "Elyn HAN", 6 | "dc_issued": "2018", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2018/05/2018_3.5_1er_CAN.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2018_D.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a707c202386e11ee88f7b705d6ffcd0a", 3 | "dc_title": "13e Prix - Catégorie 10-13 ans", 4 | "dc_spatial": "Chine", 5 | "dc_creator": "Ka Hang KWOK", 6 | "dc_issued": "2018", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2018/05/2018_10.13_13e_CHN.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2018_B.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "9173da34386e11ee90e31bb3eed21e3b", 3 | "dc_title": "9e Prix - Catégorie 3-5 ans", 4 | "dc_spatial": "Indonésie", 5 | "dc_creator": "Jorge Bentley Chandra", 6 | "dc_issued": "2018", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2018/05/2018_3.5_9e_IDN.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2018_C.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "9a7905fa386e11ee90a5bb6241e80425", 3 | "dc_title": "13e Prix - Catégorie 6-9 ans", 4 | "dc_spatial": "Almeida, Brésil", 5 | "dc_creator": "Leticia Mansano", 6 | "dc_issued": "2018", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2018/05/2018_6.9_13e_BRA.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2019_C.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "d2b49a6a386e11ee8b464fb7626a2c27", 3 | "dc_title": "7e prix - Catégorie 10-13 ans", 4 | "dc_spatial": "Ukraine", 5 | "dc_creator": "Sofiya BORODAVKA", 6 | "dc_issued": "2019", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2019/11/2019_10-13_07_UKR_R_A.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/DiscreeteDropdown.css: -------------------------------------------------------------------------------- 1 | .discreete-dropdown { 2 | visibility: hidden; 3 | } 4 | 5 | :is(.runningHead .scholium, .scholium .content, .main):hover .discreete-dropdown { 6 | visibility: visible; 7 | } 8 | 9 | .discreete-dropdown .toggle { 10 | margin-top: 0.25rem; 11 | color: crimson; 12 | cursor: pointer; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2019_B.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "bc1eb592386e11ee8601d33e7e2de576", 3 | "dc_title": "15e prix - Catégorie 10-13 ans", 4 | "dc_spatial": "Estonie", 5 | "dc_creator": "Allika Inkeri MOSER", 6 | "dc_issued": "2019", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2019/11/2019_10-13_15_EST_R_A.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2019_D.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "fe8beb2a386e11ee9dec4735e408414e", 3 | "dc_title": "21e prix - Catégorie 10-13 ans", 4 | "dc_spatial": "Chine", 5 | "dc_creator": "Sheung Kiu Chloe MO", 6 | "dc_issued": "2019", 7 | "text": "![](https://www.centre-unesco-troyes.org/wp-content/uploads/2019/11/2019_10-13_21_CHN_R_A.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/Bookshelf.css: -------------------------------------------------------------------------------- 1 | .bookshelf .card { 2 | background-color: ghostwhite; 3 | } 4 | .bookshelf .btn { 5 | color: rgb(81, 81, 81); 6 | } 7 | .bookshelf .btn-check:checked + .btn { 8 | color: black; 9 | font-weight: 600; 10 | background-color: #d5aa40; 11 | } 12 | 13 | .bookshelf #title { 14 | font-size: 20px; 15 | font-weight: 600; 16 | } -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_2019_A.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "b33f9568386e11eea7644766f8f7218a", 3 | "dc_title": "16e prix - Catégorie 10-13 ans", 4 | "dc_spatial": "Ukraine", 5 | "dc_creator": "Yelena SOROCHINSKAYA", 6 | "dc_issued": "2019", 7 | "text": "![2019_10-13_16_UKR_R_A](https://www.centre-unesco-troyes.org/wp-content/uploads/2019/11/2019_10-13_16_UKR_R_A.jpg)" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/scenarios/add_picture.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Ajouter une image à une glose 4 | 5 | Scénario: dont on est l'auteur 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand j'essaie d'ajouter une image à une glose 10 | Alors je vois l'image "" dans la glose 11 | -------------------------------------------------------------------------------- /requirements/.greenframe.yml: -------------------------------------------------------------------------------- 1 | projectName: Hyperglosae 2 | baseURL: http://localhost 3 | scenarios: 4 | - name: Bookshelf 5 | path: ./impact_of_bookshelf.js 6 | - name: Parallel texts 7 | path: ./impact_of_parallel_texts.js 8 | containers: 9 | - hyperglosae-proxy-1 10 | databaseContainers: 11 | - hyperglosae-frontend-1 # Local network 12 | - hyperglosae-backend-1 13 | 14 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/content/map.js: -------------------------------------------------------------------------------- 1 | function (doc) { 2 | const { getRelatedDocuments, emitPassages, emitIncludedDocuments } = require('views/lib/links'); 3 | 4 | let { _id, text = '', isPartOf = _id, links = [] } = doc; 5 | let related = getRelatedDocuments({isPartOf, links}); 6 | 7 | emitPassages({text, isPartOf, related}); 8 | emitIncludedDocuments({isPartOf, links}); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/scenarios/bookmark_document.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Ajouter un document à sa bibliothèque 4 | 5 | Scénario: 6 | 7 | Soit un document dont je ne suis pas l'auteur affiché comme document principal 8 | Et une session active avec mon compte 9 | Quand j'ajoute le document principal à ma bibliothèque 10 | Alors le document apparaît dans ma bibliothèque 11 | 12 | -------------------------------------------------------------------------------- /requirements/impact_of_bookshelf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Playwright scenario to be used with GreenFrame 3 | */ 4 | 5 | module.exports = async (page) => { 6 | 7 | await page.goto("", { 8 | waitUntil: 'networkidle0' 9 | }); 10 | await page.waitForTimeout(10000); 11 | await page.scrollToEnd(); 12 | await page.waitForNetworkIdle(); 13 | await page.waitForTimeout(7000); 14 | 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hyperglosae 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/all_documents/map.js: -------------------------------------------------------------------------------- 1 | function (metadata) { 2 | 3 | if (metadata.bookmark) { 4 | emit([metadata.editors[0], metadata.bookmark], {_id: metadata.bookmark}); 5 | return; 6 | } 7 | 8 | if (!metadata.dc_title) return; 9 | 10 | const editors = metadata.editors || ['PUBLIC']; 11 | editors.forEach(editor => { 12 | emit([editor, metadata._id]); 13 | }); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky09.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "d077abaa733111ed8acbdfaab48a5966", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{9} TANULSÁG\nA gyémántot és aranyat\nNagy becsben tartják az emberek;\nDe a szívélyes szavak\nNáluk is meggyőzőbben hatnak,\nÉs többet érnek." 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/menu-items/SignOutAction.jsx: -------------------------------------------------------------------------------- 1 | import Dropdown from 'react-bootstrap/Dropdown'; 2 | 3 | function SignOutAction({setUser, backend}) { 4 | 5 | const handleSignOut = () => { 6 | backend.deleteSession(); 7 | setUser(); 8 | }; 9 | 10 | return ( 11 | 12 | Sign out 13 | 14 | ); 15 | } 16 | 17 | export default SignOutAction; 18 | 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova09.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "7bfff9c46c2411eda80f5bc9281462ba", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{9} PONAUČENIE\nDiamanty a pištole majú veľkú moc nad mysľou,\nAle väčšiu silu majú milé slová,\nktoré sú ešte cennejšie." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_glossaire.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4c81f14e0bfe11f08335ab98f4298b8a", 3 | "dc_title": "Glossaire", 4 | "dc_creator": "Projet Perrault", 5 | "dc_issued": "2025-03-28", 6 | "editors": ["alice"], 7 | "links": [{ 8 | "verb": "refersTo", 9 | "object": "09c906c6732b11ed89466ba197585f87" 10 | }, { 11 | "verb": "refersTo", 12 | "object": "420ab198674f11eda3b7a3fdd5ea984f" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/metadata/map.js: -------------------------------------------------------------------------------- 1 | function ({_id, links = [], dc_title}) { 2 | const { parseReference } = require('views/lib/links'); 3 | 4 | if (!dc_title) return; 5 | links.forEach(({subject, object}, i) => { 6 | let reference = parseReference((subject && subject !== _id) ? subject : object).id; 7 | emit([reference], {_id}); 8 | emit([_id, i], {_id: reference}); 9 | }); 10 | emit([_id], {_id}); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/menu-items/EditRawDocumentAction.jsx: -------------------------------------------------------------------------------- 1 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 2 | 3 | function EditRawDocumentAction({setRawEditMode}) { 4 | 5 | const handleClick = () => setRawEditMode(true); 6 | 7 | return ( 8 | 9 | Edit passage numbering 10 | 11 | ); 12 | } 13 | 14 | export default EditRawDocumentAction; 15 | 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_snz113.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "b8cc79d8abba11edb9ee53989bc96c06", 3 | "dc_title": "Photographie : vitrail, baie 113", 4 | "dc_spatial": "Église Saint-Nizier, Troyes", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2016-07-10", 7 | "text": "![SNZ 113](https://steatite.utt.fr/optimized/2520b08208040a0a6b1b5aad41cca55de0618c57)", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/" 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky10.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "12deb8da733211eda47ff75aed060716", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{10} MÁSIK TANULSÁG\nA szívélyesség fáradozással jár,\nÉs önzetlen jóság kell hozzá;\nDe előbb vagy utóbb elnyeri jutalmát,\nÉs gyakran olyankor, mikor az ember mit sem vár." 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_grv005_85.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4745d83eabc111edb4d7a3e38e32ff69", 3 | "dc_title": "Photographie : vitrail, baie 5", 4 | "dc_spatial": "Église Saint-Martin, Grandville, Aube", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2016-07-02", 7 | "text": "![GRV 005](https://steatite.utt.fr/optimized/85274046873c9bcb7ee4563e6e6ec3506b505673)", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/" 9 | } 10 | -------------------------------------------------------------------------------- /requirements/impact_of_parallel_texts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Playwright scenario to be used with GreenFrame 3 | */ 4 | 5 | module.exports = async (page) => { 6 | 7 | await page.goto("37b4b9ba5cdb11ed887beb5c373fa643#09c906c6732b11ed89466ba197585f87", { 8 | waitUntil: 'networkidle0' 9 | }); 10 | await page.waitForTimeout(10000); 11 | await page.scrollToEnd(); 12 | await page.waitForNetworkIdle(); 13 | await page.waitForTimeout(7000); 14 | 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_appreciation.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b123456", 3 | "dc_title": "Appréciation", 4 | "dc_creator": "Aurélien Bénel", 5 | "editors": ["benel"], 6 | "dc_issued": "2023-02-21", 7 | "text": "14/20", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 9 | "links": [{ 10 | "verb": "refersTo", 11 | "object": "4e1a31e14b032f2fa9e161ee9b009122" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_smv006_12.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "392618eaabc111eda58c4b41919f9718", 3 | "dc_title": "Photographie : vitrail, baie 6", 4 | "dc_spatial": "Église Saint-Martin-ès-Vignes, Troyes", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2020-12-13", 7 | "text": "![SMV 006 Lavage](https://steatite.utt.fr/optimized/1220c6f1f1d0c3c512df8a5c933281f7c23145b6)", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/" 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_smv006_7f.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "23ee16c6abc111edacc14f3cacd3cada", 3 | "dc_title": "Photographie : vitrail, baie 6", 4 | "dc_spatial": "Église Saint-Martin-ès-Vignes, Troyes", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2020-12-13", 7 | "text": "![SMV 006 Soleil](https://steatite.utt.fr/optimized/7ffa65506f5a64822f0499dedf8cda5dd3588522)", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/" 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova10.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "52d830ca6c1511ed982213e7a70f2ec4", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{10} ĎAĽŠIE PONAUČENIE\nÚprimnosť stojí veľa námahy,\nžiada si trochu porozumenia,\nAle skôr či neskôr sa jej dostane odmeny,\nA často vtedy, keď to najmenej čakáme." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_E.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "c2b9f52285ce11edbd0aff9b25defbab", 3 | "dc_title": "Analyse de l'entretien", 4 | "dc_isPartOf": "Archéologie préventive (IF14)", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2019-09-24T15:49:34.379Z", 7 | "links": [{ 8 | "verb": "refersTo", 9 | "object": "6327c5008d1f11ed9aa8e7ae771dee2e" 10 | }], 11 | "dc_language": "french", 12 | "dc_license": "https://creativecommons.org/licenses/by/4.0/" 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/styles/Metadata.css: -------------------------------------------------------------------------------- 1 | .work { 2 | font-variant: small-caps; 3 | margin-right: .5rem; 4 | } 5 | 6 | .edition { 7 | padding-bottom: 12px; 8 | } 9 | 10 | .license a { 11 | text-decoration: none; 12 | color: inherit; 13 | } 14 | 15 | .license a:hover { 16 | color: inherit; 17 | } 18 | 19 | .icon { 20 | opacity: 1 !important; 21 | cursor: pointer; 22 | transition: filter 0.3s ease; 23 | } 24 | 25 | .icon:hover { 26 | filter: brightness(1.5); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_glossaire_content.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "66c32c560c0011f086923bf64fdf201f", 3 | "isPartOf": "4c81f14e0bfe11f08335ab98f4298b8a", 4 | "editors": ["alice"], 5 | "text": "\"Il était une fois\"\n: \"Once upon a time\" (eng)\n: \"Bolo to raz\" (svk)", 6 | "links": [{ 7 | "verb": "refersTo", 8 | "object": "420ab198674f11eda3b7a3fdd5ea984f" 9 | }, { 10 | "verb": "refersTo", 11 | "object": "09c906c6732b11ed89466ba197585f87" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky03.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "45a3cf42732f11ed8155b7385c2ad919", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{3} – Szíves-örömest, anyóka – mondta a szép lány; kiöblítette a korsóját, a forrás legtisztább helyén megmerítette, és úgy nyújtotta az asszony felé, kezével megtámasztva, hogy az kényelmesen ihasson belőle." 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_1697.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "02ee00d85cdb11ed834c4fb9e3c972af", 3 | "dc_title": "Les fées : Conte", 4 | "dc_creator": "Charles Perrault", 5 | "dc_isPartOf": "Histoires ou contes du temps passé avec des moralitez", 6 | "dc_publisher": "Claude Barbin", 7 | "dc_issued": "1697", 8 | "dc_language": "17th c. french", 9 | "dc_source": "https://fr.wikisource.org/wiki/Page:Perrault_-_Histoires_ou_contes_du_temps_passé,_avec_des_moralitez,_1697.djvu/115" 10 | } 11 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "HyperGlosae", 3 | "name": "HyperGlosae", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "1428x298" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .screen { 16 | margin: 12px 0; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova03.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a1cc62346c1311ed8cbd639d185ff2db", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{3} —Áno, rada, milá starenka, povedala krásna dievčina a hneď opláchla džbán, nabrala doň vodu z najkrajšieho miesta v studni a podala jej ho. Džbán naďalej pridržiavala, aby mohla starena ľahšie piť." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography03.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "6b56ee657c870dfacd34e9ae4e05dbd2", 3 | "links": [ 4 | { 5 | "verb": "refersTo", 6 | "object": "6b56ee657c870dfacd34e9ae4e050fcc" 7 | } 8 | ], 9 | "dc_issued": "2012", 10 | "dc_language": "french", 11 | "dc_title": "Rencontrons un président", 12 | "text": "* Origine, d'où ? \n* Train électrique \n* Milieu des cheminots\n* En faire un travail \n* Trains touritiques\n", 13 | "dc_creator": "Christophe Lejeune" 14 | } 15 | -------------------------------------------------------------------------------- /frontend/scenarios/include_video.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Insérer dans un document une vidéo 4 | 5 | Scénario: provenant de YouTube 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand j'essaie de remplacer le contenu de la glose par : 10 | """ 11 | ![link](https://www.youtube.com/watch?v=JRXkAhMYKEc&ab_channel=ViniciusHenrique) 12 | """ 13 | Alors le document comporte la vidéo "https://www.youtube.com/embed/JRXkAhMYKEc" 14 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "35d34a186c1611edbc2373a6ebb775ad", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{2} Úbohé dievča muselo tiež chodiť dvakrát denne po vodu viac ako pol míle od domu a vždy odtiaľ priniesť plný veľký džbán. Jedného dňa, keď bola pri studni, prišla k nej chudobná žena a poprosila ju, či by jej nedala napiť." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_note_rire_El2.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b009123", 3 | "dc_title": "Note rire Buster Keaton", 4 | "dc_creator": "Sun Sun", 5 | "dc_issued": "2023-02-21", 6 | "text": "Ce qui m'a fait rire :\n\n00:09:40.000 --> 00:10:15.000\n\nIl tombe dans son propre piège (peau de banane).", 7 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 8 | "links": [{ 9 | "verb": "refersTo", 10 | "object": "4e1a31e14b032f2fa9e161ee9b009125" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_expo_enfants.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a0cdf96ab2c211ed9f5ecfb8295a0b31", 3 | "dc_title": "Exposition \"Nos dessins préférés\"", 4 | "dc_isPartOf": "", 5 | "dc_creator": "Léa et Léo", 6 | "dc_issued": "2022-10-13", 7 | "text": "", 8 | "links": [ { 9 | "verb": "includes", 10 | "object": "8844c7de386e11eeb84fc79a7aaea501" 11 | }, { 12 | "verb": "includes", 13 | "object": "9173da34386e11ee90e31bb3eed21e3b" 14 | }, { 15 | "verb": "includes", 16 | "object": "d2b49a6a386e11ee8b464fb7626a2c27" 17 | }] 18 | } 19 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_C.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "05b61f5285c711ed97bf6b9b56808c45", 3 | "dc_title": "Entretien avec un responsable d'opération", 4 | "dc_isPartOf": "Archéologie préventive (IF14)", 5 | "dc_creator": "Diane P.", 6 | "editors": [ 7 | "diane" 8 | ], 9 | "dc_issued": "2014", 10 | "links": [{ 11 | "verb": "refersTo", 12 | "object": "564cf7f485c411edba2c6fe0f6ec7a8f" 13 | }], 14 | "dc_language": "french", 15 | "dc_license": "", 16 | "dc_source": "https://cassandre.utt.fr/memo/edbb69a33827773d728069d87556d2e7" 17 | } 18 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "1d9efeaa732e11eda85fb3153ffd2f13", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{2} Ennek a szegény gyermeknek egyéb teendői mellett naponta kétszer el kellett mennie a házuktól jó fél mérföldnyire található forráshoz, és egy színültig megtöltött, nagy korsó vizet hazacipelnie. Egy nap, amikor a forrásnál járt, egy szegény asszony megszólította, és inni kért tőle." 9 | } 10 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/validate_doc_update.js: -------------------------------------------------------------------------------- 1 | function (_, oldDoc, user) { 2 | //See https://docs.couchdb.org/en/stable/ddocs/ddocs.html#validate-document-update-functions 3 | 4 | function advise(todo) { 5 | throw({unauthorized: `Before editing this document, please ${todo} first.`}); 6 | } 7 | 8 | if (user.roles.includes('_admin')) return true; 9 | 10 | if (!user.name) advise('log in'); 11 | 12 | if (oldDoc && oldDoc.editors && !oldDoc.editors.includes(user.name)) { 13 | advise('request authorization to its editors'); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_mimes.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b00789", 3 | "dc_title": "Mimes Buster Keaton", 4 | "dc_creator": "Agathe Allirol--Binet", 5 | "dc_issued": "2023-02-21", 6 | "text": " [![](https://www.puy-de-dome.fr/fileadmin/user_upload/CeC1718_Stagetheatre_12dec_StAntheme_-_113.jpg)](https://www.youtube.com/watch?v=JRXkAhMYKEc)", 7 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 8 | "links": [{ 9 | "verb": "adapts", 10 | "object": "4e1a31e14b032f2fa9e161ee9b009125" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_1886.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "37b4b9ba5cdb11ed887beb5c373fa643", 3 | "dc_title": "Les fées", 4 | "dc_creator": "Charles Perrault", 5 | "dc_isPartOf": "Contes des fées", 6 | "dc_publisher": "Beauchemin et Valois, libraires-imprimeurs à Montréal", 7 | "dc_issued": "1886", 8 | "dc_language": "french", 9 | "dc_license": "Public domain", 10 | "dc_source": "https://fr.wikisource.org/wiki/Page:Perrault_-_Contes_des_fées,_1886.djvu/100", 11 | "links": [{ 12 | "verb": "adapts", 13 | "object": "02ee00d85cdb11ed834c4fb9e3c972af" 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "6b56ee657c870dfacd34e9ae4e0576ce", 3 | "links": [ 4 | { 5 | "verb": "refersTo", 6 | "object": "6b56ee657c870dfacd34e9ae4e050fcc" 7 | } 8 | ], 9 | "dc_issued": "2012", 10 | "dc_language": "french", 11 | "dc_title": "Se rendre à l'atelier", 12 | "dc_creator": "Christophe Lejeune", 13 | "text": "\n * Dans l'atelier / en ligne / au musée / sur le faisceau \n* Chacun de son côté / en sous groupe / en grand groupe \n* Apprendre / transmettre / coacher \n* Parrainage / mentorat / compagnonnage" 14 | } 15 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_bintizulkiflee.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "96bddee6-24b2-11ef-a1f4-5351a35fcaac", 3 | "dc_title": "Fairies", 4 | "dc_creator": "Charles Perrault", 5 | "dc_translator": "Nizreen Ana Binti Zulkiflee", 6 | "dc_issued": "2024-06-07", 7 | "dc_language": "english", 8 | "dc_license": "https://creativecommons.org/licenses/by-nc-nd/4.0/", 9 | "links": [{ 10 | "verb": "adapts", 11 | "object": "420ab198674f11eda3b7a3fdd5ea984f" 12 | }], 13 | "editors": ["alice"], 14 | "text": "{1} Once upon a time were a widow and her two daughters." 15 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | backend: 4 | extends: 5 | file: docker-compose.test.yml 6 | service: backend 7 | volumes: 8 | - ./data:/opt/couchdb/data 9 | 10 | updated_code: 11 | extends: 12 | file: docker-compose.test.yml 13 | service: updated_code 14 | depends_on: 15 | backend: 16 | condition: service_healthy 17 | 18 | frontend: 19 | image: benel/hyperglosae 20 | 21 | proxy: 22 | extends: 23 | file: docker-compose.test.yml 24 | service: proxy 25 | depends_on: 26 | - backend 27 | - frontend 28 | 29 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "09c906c6732b11ed89466ba197585f87", 3 | "dc_title": "A tündérek", 4 | "dc_creator": "Charles Perrault", 5 | "dc_translator": ["Ildikó Lőrinszky", "Laurent Dedryvère"], 6 | "dc_issued": "2022-07-17", 7 | "dc_language": "hungarian", 8 | "dc_license": "https://creativecommons.org/licenses/by-nc-nd/4.0/", 9 | "dc_source": "https://traduxio.org/works/79df781ff8941879f960c6500b3b74cf?open=Ildikó%20Lőrinszky%20et%20Laurent%20Dedryvère", 10 | "links": [{ 11 | "verb": "adapts", 12 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/menu-items/CommentFragmentAction.jsx: -------------------------------------------------------------------------------- 1 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 2 | 3 | function CommentFragmentAction({selectedText, setSelectedText, setFragment, margin}) { 4 | 5 | const disabled = !selectedText || !margin; 6 | 7 | const handleClick = () => { 8 | setFragment(`[${selectedText}]\n`); 9 | setSelectedText(); 10 | }; 11 | 12 | return ( 13 | 14 | Comment the selected text... 15 | 16 | ); 17 | } 18 | 19 | export default CommentFragmentAction; 20 | 21 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_expo_2018.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a0cdf96ab2c211ed9f5acfb8095a0b31", 3 | "dc_title": "Exposition \"La paix, le bien vivre ensemble\"", 4 | "dc_creator": "IMAJ", 5 | "dc_issued": "2018", 6 | "text": "", 7 | "links": [ { 8 | "verb": "includes", 9 | "object": "8844c7de386e11eeb84fc79a7aaea501" 10 | }, { 11 | "verb": "includes", 12 | "object": "9173da34386e11ee90e31bb3eed21e3b" 13 | }, { 14 | "verb": "includes", 15 | "object": "9a7905fa386e11ee90a5bb6241e80425" 16 | }, { 17 | "verb": "includes", 18 | "object": "a707c202386e11ee88f7b705d6ffcd0a" 19 | }] 20 | } 21 | -------------------------------------------------------------------------------- /samples/hyperglosae/imaj_expo_2019.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a0cdf96ab2c211ed9f5ecfb8295a0b66", 3 | "dc_title": "Exposition \"Traces et écritures dans l'Histoire\"", 4 | "dc_creator": "IMAJ", 5 | "dc_issued": "2019", 6 | "text": "", 7 | "links": [ { 8 | "verb": "includes", 9 | "object": "b33f9568386e11eea7644766f8f7218a" 10 | }, { 11 | "verb": "includes", 12 | "object": "bc1eb592386e11ee8601d33e7e2de576" 13 | }, { 14 | "verb": "includes", 15 | "object": "d2b49a6a386e11ee8b464fb7626a2c27" 16 | }, { 17 | "verb": "includes", 18 | "object": "fe8beb2a386e11ee9dec4735e408414e" 19 | }] 20 | } 21 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova04.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "1175f8366c1711edae39035884521923", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{4} Keď starena dopila, povedala dievčine:\n— Si taká krásna, taká dobrá a taká slušná, že ťa nemôžem neobdarovať (bola to totiž víla, ktorá sa premenila na chudobnú ženu z dediny, aby zistila, kam až siaha slušnosť dievčaťa). Toto ti dávam ako dar, pokračovala víla, pri každom slove, ktoré povieš, ti z úst vyjde buď kvet, alebo drahokam." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky04.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "f3a8b828732f11ed9bca072a9dbf9775", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{4} A jóasszony ivott, majd így szólt:\n– Te oly szép vagy, oly jó, oly szívélyes, hogy mindenképpen szeretnélek megajándékozni valamivel (mert valójában tündér volt, aki azért változott szegény parasztasszonnyá, hogy lássa, mennyire jószívű a lány). Azt adom neked ajándékul – folytatta a tündér –, hogy valahányszor megszólalsz, virág vagy drágakő fog kihullni a szádból." 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_washing.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "af81c39e-b2c5-11ed-8c9a-8b6034d3ea69", 3 | "dc_title": "Lavage des tuniques – Comparaison de vitraux", 4 | "dc_isPartOf": "Iconographie et numérique : De la sémantique à la sémiotique", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2022-10-13", 7 | "text": "", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 9 | "links": [{ 10 | "verb": "includes", 11 | "object": "b8cc79d8abba11edb9ee53989bc96c06#xywh=percent:29.8,34.4,12.5,26" 12 | }, { 13 | "verb": "includes", 14 | "object": "392618eaabc111eda58c4b41919f9718" 15 | }] 16 | } 17 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "6b56ee657c870dfacd34e9ae4e050fcc", 3 | "dc_issued": "2012", 4 | "dc_language": "french", 5 | "dc_title": "Restaurer la vapeur", 6 | "dc_creator": "Christophe Lejeune", 7 | "text": "Comment se coordonne la restauration de locomotives à vapeur ? Comment les bénévoles acquièrent-ils collectivement les compétences techniques et les habiletés sociales permettant de faire rouler chaque semaine des locomotives à vapeur ? Comment se construit l'identité de celui ou celle qui est prêt-e à passer ses week-ends à poncer des essieux de locomotives ? Comment cet investissement se maintient-il dans la durée ?" 8 | } 9 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "420ab198674f11eda3b7a3fdd5ea984f", 3 | "dc_title": "Víly", 4 | "dc_creator": "Charles Perrault", 5 | "dc_translator": ["Diana Jamborova Lemay", "Étudiants de slovaque à l'Inalco"], 6 | "dc_issued": "2022-06-20", 7 | "dc_language": "slovak", 8 | "dc_license": "https://creativecommons.org/licenses/by-nc-nd/4.0/", 9 | "dc_source": "https://traduxio.org/works/79df781ff8941879f960c6500b3b74cf?open=Diana%20Jamborova%20Lemay%20avec%20les%20étudiants%20de%20slovaque%20à%20l%27Inalco", 10 | "editors": ["alice"], 11 | "links": [{ 12 | "verb": "adapts", 13 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_E_content03.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "291373b685d411ed9e29c31843b32fd3", 3 | "isPartOf": "c2b9f52285ce11edbd0aff9b25defbab", 4 | "text": "## Actions\nOn remarque que les actions sont principalement de deux ordres :\n- celles liées à la matérialité de la fouille, de la terre, des vestiges ([décaper], [creuser], [tamiser], [prélever], nettoyer, [mettre en sac], étiqueter),\n- celles liées à la production de documents (faire le relevé, [photographier], [dessiner les plans], rédiger).\n", 5 | "dc_source": "https://cassandre.utt.fr/memo/f413206cef01d9b99ebb3b6de20559fb", 6 | "links": [{ 7 | "verb": "refersTo", 8 | "object": "6327c5008d1f11ed9aa8e7ae771dee2e" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/styles/Type.css: -------------------------------------------------------------------------------- 1 | .typeBadge { 2 | margin: 10px 5px; 3 | text-transform: capitalize; 4 | width: fit-content; 5 | padding: 8px 20px!important; 6 | font-size: 12px; 7 | font-weight: bold; 8 | line-height: 1; 9 | color: white; 10 | text-align: center; 11 | vertical-align: baseline; 12 | border-radius: 20px; 13 | display: inline-block; 14 | word-break: break-word; 15 | } 16 | 17 | .typeIcon { 18 | opacity: 1 !important; 19 | cursor: pointer; 20 | transition: filter 0.3s ease; 21 | display: inline-block !important; 22 | visibility: visible !important; 23 | } 24 | 25 | .typeIcon:hover { 26 | filter: brightness(1.5); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/context.js: -------------------------------------------------------------------------------- 1 | function Context(id, metadata) { 2 | metadata = metadata || []; 3 | 4 | this.getDocument = (x) => metadata.find(y => (y._id === x)) || {}; 5 | 6 | this.focusedDocument = this.getDocument(id); 7 | 8 | const forwardLinks = (this.focusedDocument.links || []) 9 | .map(({subject, object}) => (subject && (subject !== id)) ? subject : object) 10 | .map(x => x.split('#')[0]); 11 | 12 | this.forwardLinkedDocuments = metadata.filter( 13 | x => forwardLinks.includes(x._id) 14 | ); 15 | 16 | this.reverseLinkedDocuments = metadata.filter( 17 | x => !forwardLinks.includes(x._id) && x._id !== id 18 | ); 19 | 20 | return this; 21 | } 22 | 23 | export default Context; 24 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_note_rire_El1.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b009124", 3 | "dc_title": "Note rire Buster Keaton", 4 | "dc_creator": "Antoine-Valentin Charpentier", 5 | "dc_issued": "2023-02-21", 6 | "text": "Ce qui m'a fait rire :\n\n00:03:09.000 --> 00:03:15.000\n\nUne feuille de papier est collé sur le balais.\n\n00:09:40.000 --> 00:10:15.000\n\nIl tombe dans son propre piège (peau de banane).\n\n00:14:18.000 --> 00:14:32.000\n\nLes personnes se suivent de très proche.\n", 7 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 8 | "links": [{ 9 | "verb": "refersTo", 10 | "object": "4e1a31e14b032f2fa9e161ee9b009125" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/updates/document.js: -------------------------------------------------------------------------------- 1 | function(doc, req) { 2 | const body = JSON.parse(req.body, (k, v) => (k === 'history') ? undefined : v); 3 | if (!doc) { 4 | doc = Object.assign({}, body, { 5 | _id: req.id, 6 | _rev: req.rev, 7 | history: [{ 8 | user: req.userCtx.name, 9 | date: new Date().toISOString(), 10 | action: 'creation', 11 | }], 12 | }); 13 | } else { 14 | doc = Object.assign({}, doc, body); 15 | if (!doc.history) doc.history = []; 16 | doc.history.push({ 17 | user: req.userCtx.name, 18 | date: new Date().toISOString(), 19 | action: 'modification', 20 | }); 21 | } 22 | return [doc, { json: { status: 'ok' } }]; 23 | } 24 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_E_content02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "9db7d73c85d111eda065a73618279ea5", 3 | "isPartOf": "c2b9f52285ce11edbd0aff9b25defbab", 4 | "text": "## Acteurs\nLes acteurs mentionnés sont nombreux et divers. Se rencontrent :\n- des acteurs du monde de la recherche (Histoire, Histoire de l'Art...),\n- des acteurs du monde des administrations publiques (Culture, Patrimoine).\n\nC'est peut-être parce que le patrimoine est un objet hybride :\n- à conserver (Ministère de la Culture),\n- à étudier (Ministère de la Recherche).\n", 5 | "dc_source": "https://cassandre.utt.fr/memo/f413206cef01d9b99ebb3b6de205c294", 6 | "links": [{ 7 | "verb": "refersTo", 8 | "object": "6327c5008d1f11ed9aa8e7ae771dee2e" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/revelation_stars.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a0cdf96ab2c211ed9f5ecfb8095a0b31", 3 | "dc_title": "Soleil noir et étoiles qui tombent – Comparaison de vitraux", 4 | "dc_isPartOf": "Iconographie et numérique : De la sémantique à la sémiotique", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2022-10-13", 7 | "text": "", 8 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 9 | "links": [{ 10 | "verb": "includes", 11 | "object": "b8cc79d8abba11edb9ee53989bc96c06#xywh=percent:28,59.3,13.7,38.4" 12 | }, { 13 | "verb": "includes", 14 | "object": "23ee16c6abc111edacc14f3cacd3cada" 15 | }, { 16 | "verb": "includes", 17 | "object": "4745d83eabc111edb4d7a3e38e32ff69" 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "06637e92732d11ed9ecf9b535a415b2b", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{1} Volt egyszer egy özvegyasszony, s annak két lánya. A nagyobbik kívül-belül annyira hasonlított rá, hogy aki ránézett, anyjára ismert benne. Olyan undok és gőgös volt mindkettő, hogy senki sem tudott megmaradni mellettük. A kisebbik az apjára ütött, szelíd volt és becsületes, s emellett szépsége is párját ritkította. Mivel mindenki ahhoz vonzódik, aki hasonlít rá, az anya bolondult a nagyobbik lányáért, a kisebbiket pedig gyűlölte. A konyhában kellett ennie, és egész álló nap dolgoznia." 9 | } 10 | -------------------------------------------------------------------------------- /frontend/scenarios/comment_fragment.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Créer un commentaire à partir d'un fragment 4 | 5 | Scénario: de texte 6 | 7 | Soit "Treignes, le 8 septembre 2012 (Christophe Lejeune)" le document principal 8 | Et un autre document, en plusieurs passages, affiché comme glose et dont je suis l'auteur 9 | Et une session active avec mon compte 10 | Quand je sélectionne le fragment de texte : 11 | """ 12 | plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue 13 | """ 14 | Alors la glose est ouverte en mode édition et contient : 15 | """ 16 | [plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue] 17 | … 18 | """ 19 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky05.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "cba00038733011edbca8132c5db7ee3f", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{5} Amikor a szép lány hazaért, anyja megszidta, amiért oly sokáig elmaradt a forrásnál.\n– Bocsásson meg, anyám, hogy ennyire megkéstem – mondta a szegény lány; s közben két rózsaszál, két igazgyöngy és két nagy gyémánt buggyant ki a szájából.\n– Mit látnak szemeim? – kérdezte csodálkozva az anya. – Mintha igazgyöngy meg gyémánt hullna a szájából! Hogy lehet ez, lányom?\n(Ekkor szólította először a lányának.)\nA szegény gyermek őszintén elmesélte neki, mi történt vele, s közben egyfolytában potyogtak szájából a gyémántok." 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky08.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "a0e2695c733111eda8b743b46aeddbbc", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{8} A királyfi látta, hogy eközben öt-hat igazgyöngy és ugyanennyi gyémánt hullt ki a lány szájából, és megkérdezte, hogyan lehetséges ez. A lány elmesélte neki az egész kalandját. A királyfi beleszeretett, és mivel úgy találta, hogy az ilyen ajándék többet ér, mint bármilyen más hozomány, magával vitte a lányt apja palotájába, és feleségül vette. Az idősebbik lány olyan utálatos lett, hogy még tulajdon anyja sem tűrte meg többé a házában; a nyomorult teremtés sokáig bolyongott, de senki sem fogadta be, az erdő szélén halt meg." 9 | } 10 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_A.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "f413206cef01d9b99ebb3b6de2007ca7", 3 | "dc_title": "Choix du sujet", 4 | "dc_isPartOf": "Archéologie préventive (IF14)", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2019-09-10T15:13:40.834Z", 7 | "text": "L'archéologie préventive a pour vocation d'étudier et éventuellement de préserver le patrimoine archéologique menacé par des travaux d'aménagement.\n\nEn première approche, si on devait modéliser la fouille archéologique en termes de flux, on pourrait considérer qu'elle transforme la terre d'une parcelle en un ensemble d'objets et de connaissances archéologiques.", 8 | "dc_language": "french", 9 | "dc_license": "https://creativecommons.org/licenses/by/4.0/", 10 | "dc_source": "https://cassandre.utt.fr/memo/f413206cef01d9b99ebb3b6de2007ca7" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/DiscreeteDropdown.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/DiscreeteDropdown.css'; 2 | 3 | import { forwardRef } from 'react'; 4 | import { Dropdown } from 'react-bootstrap'; 5 | import { ThreeDotsVertical } from 'react-bootstrap-icons'; 6 | 7 | function DiscreeteDropdown({children}) { 8 | 9 | const Toggle = forwardRef(({onClick}, ref) => ( 10 | 11 | )); 12 | Toggle.displayName = 'Toggle'; 13 | 14 | return ( 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | ); 22 | } 23 | 24 | DiscreeteDropdown.Item = Dropdown.Item; 25 | 26 | export default DiscreeteDropdown; 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Download sources 11 | uses: actions/checkout@v4 12 | - name: Set up NodeJS 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | cache: 'npm' 17 | cache-dependency-path: frontend/package-lock.json 18 | - name: Install frontend dependencies 19 | run: | 20 | cd frontend 21 | npm install 22 | - name: Build frontend 23 | run: | 24 | cd frontend 25 | npm run build 26 | - name: Save frontend build for later 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: frontend-build 30 | path: frontend/build 31 | -------------------------------------------------------------------------------- /frontend/cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import createBundler from '@bahmutov/cypress-esbuild-preprocessor'; 3 | import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'; 4 | import { createEsbuildPlugin } from '@badeball/cypress-cucumber-preprocessor/esbuild'; 5 | 6 | export default defineConfig({ 7 | e2e: { 8 | baseUrl: 'http://localhost:3000', 9 | specPattern: '**/*.feature', 10 | supportFile: 'tests/support.js', 11 | defaultCommandTimeout: 6000, 12 | async setupNodeEvents(on, config) { 13 | await addCucumberPreprocessorPlugin(on, config); 14 | on( 15 | 'file:preprocessor', 16 | createBundler({ 17 | plugins: [createEsbuildPlugin(config)], 18 | }) 19 | ); 20 | return config; 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/scenarios/focus_on_document.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Se focaliser 4 | 5 | Contexte: 6 | 7 | Soit "Les fées (Charles Perrault)" le document principal 8 | Et "A tündérek (Charles Perrault)" une des gloses 9 | Et "Víly (Charles Perrault)" une des gloses 10 | Et "Les fées : Conte (Charles Perrault)" une des sources 11 | 12 | Scénario: sur la glose d'un document 13 | 14 | Quand je me focalise sur "A tündérek (Charles Perrault)" 15 | Alors "A tündérek (Charles Perrault)" est le document principal 16 | Et "Les fées (Charles Perrault)" une des sources 17 | 18 | Scénario: sur la source d'un document 19 | 20 | Quand je me focalise sur "Les fées : Conte (Charles Perrault)" 21 | Alors "Les fées : Conte (Charles Perrault)" est le document principal 22 | Et "Les fées (Charles Perrault)" une des gloses 23 | 24 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova05.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "86470e706c1711ed81a63f680c2e55c2", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{5} Keď prišla dievčina domov, matka ju vyhrešila za to, že sa tak neskoro vrátila od studne.\n— Matka, prosím, prepáčte mi, že som tak dlho meškala, povedala úbohá dievčina, a ako tak rozprávala, z úst jej vyšli dve ruže, dve perly a dva veľké diamanty.\n— Čo to vidím? povedala matka celá udivená, zdá sa mi, že jej z úst vychádzajú perly a diamanty. Odkiaľ sa to vzalo, dcéra moja? (Bolo to po prvý raz, čo ju nazvala svojou dcérou.)\nÚbohé dievča jej naivne vyrozprávalo všetko, čo sa stalo, a z úst sa jej pritom valilo nekonečné množstvo diamantov." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova08.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "2000281a6c2411ed8cf6dfdb89c145f3", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{8} Princ, ktorý videl, ako jej z úst vychádza päť alebo šesť perál a rovnako veľa diamantov, ju požiadal, aby mu povedala, odkiaľ ich má. Vyrozprávala mu celý svoj príbeh. Kráľov syn sa do nej zaľúbil, a pretože vedel, že takýto dar je cennejší ako čokoľvek iné, čo by niekto mohol darovať druhému do manželstva, zaviedol ju do paláca svojho otca, kráľa, kde sa s ňou oženil. Jej sestru tak veľmi znenávideli, že ju vlastná matka vyhnala z domu. Nešťastnica sa potom všade okolo veľa nabehala ale nenašla nikoho, kto by ju prijal, a tak odišla zomrieť na okraj lesa." 10 | } 11 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "1c007f88716711edb506f73d3d941041", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{1} Bola raz jedna vdova, ktorá mala dve dcéry. Staršia akoby matke z oka vypadla, veľmi sa jej podobala povahou i tvárou, a ktokoľvek ju videl, videl v nej jej matku. Obe boli tak nepríjemné a pyšné, že sa s nimi nedalo žiť. Mladšia dcéra bola vďaka svojej láskavosti a slušnosti verným obrazom svojho otca, ale aj jednou z najkrajších dievčat, aké kto kedy videl.\nA keďže vrana k vrane sadá a rovný rovného si hľadá, matka bola pobláznená do svojej staršej dcéry a zároveň strašne nenávidela mladšiu. Nedovolila jej jesť inde ako v kuchyni a nútila ju ustavične pracovať." 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/styles/FutureDocument.css: -------------------------------------------------------------------------------- 1 | .icon, .existingDocument { 2 | cursor: pointer; 3 | } 4 | 5 | .link-icon { 6 | font-size: 18px; 7 | } 8 | 9 | .gloses .card-header { 10 | background-color: rgb(210, 210, 210); 11 | } 12 | 13 | .gloses .nav-link.active { 14 | background-color: gainsboro !important; 15 | border-color: var(--bs-card-border-color) !important; 16 | border-bottom-color: gainsboro !important; 17 | } 18 | 19 | .nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover { 20 | border-color: var(--bs-card-border-color) !important; 21 | border-bottom-color: transparent !important; 22 | } 23 | 24 | .form-check-input:checked { 25 | background-color: gray !important; 26 | border-color: gray !important; 27 | } 28 | 29 | .document-list-container { 30 | max-height: 400px; 31 | overflow-y: auto; 32 | } 33 | 34 | .checkbox-label { 35 | word-break: break-word; 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | backend: 4 | image: couchdb:3 5 | ports: 6 | - 5984:5984 7 | environment: 8 | - COUCHDB_USER 9 | - COUCHDB_PASSWORD 10 | healthcheck: 11 | test: curl -f http://localhost:5984/_up || exit 1 12 | start_period: 5s 13 | start_interval: 2s 14 | 15 | updated_samples: 16 | image: node:22-slim 17 | command: npx couchdb-bootstrap http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@backend:5984 samples 18 | volumes: 19 | - ./samples:/samples 20 | depends_on: 21 | backend: 22 | condition: service_healthy 23 | 24 | updated_code: 25 | image: node:22-slim 26 | command: npx couchdb-bootstrap http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@backend:5984 app 27 | volumes: 28 | - ./backend:/app 29 | depends_on: 30 | backend: 31 | condition: service_healthy 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/styles/HistoryInfo.css: -------------------------------------------------------------------------------- 1 | .info-container { 2 | display: flex; 3 | justify-content: flex-end; 4 | padding-bottom: 0; 5 | } 6 | 7 | .info-icon-container { 8 | position: relative; 9 | display: inline-block; 10 | cursor: pointer; 11 | color: crimson; 12 | } 13 | .info-icon-container:hover { 14 | padding-left: 24px; 15 | } 16 | 17 | .text-document-creation { 18 | display: none; 19 | position: absolute; 20 | right: 0; 21 | top: 100%; 22 | z-index: 10; 23 | white-space: nowrap; 24 | background-color: white; 25 | border: 1px solid #ccc; 26 | padding: 6px 12px; 27 | box-shadow: 0 4px 8px rgba(0,0,0,0.1); 28 | max-height: 600px; 29 | overflow-y: auto; 30 | } 31 | 32 | .info-icon-container:hover .text-document-creation { 33 | display: block; 34 | } 35 | 36 | .separator { 37 | margin: 3px; 38 | } 39 | 40 | .btn-see-more { 41 | color : crimson !important; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/License.jsx: -------------------------------------------------------------------------------- 1 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 2 | 3 | function License({ license }) { 4 | if (license === 'Public domain') return ( 5 |
6 | Public domain 7 |
8 | ); 9 | let license_uri = license; 10 | let [license_name] = /(BY[\w-]*)/i.exec(license_uri) || []; 11 | if (license_name) return ( 12 |
13 | Consult license clauses} > 14 | 15 | {`CC-${license_name.toUpperCase()}`} 16 | 17 | 18 |
19 | ); 20 | return ( 21 | 22 | All rights reserved 23 | 24 | ); 25 | } 26 | 27 | export default License; -------------------------------------------------------------------------------- /frontend/scenarios/show_fragment.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Consulter le contexte d'un commentaire de fragment 4 | 5 | Scénario: de vidéo 6 | Soit "Vidéo Sherlock Jr. (Buster Keaton)" le document principal 7 | Et "Note rire Buster Keaton (Antoine-Valentin Charpentier)" la glose ouverte 8 | Quand je clique sur la référence temporelle "00:03:09.000 --> 00:03:15.000" annotée 9 | Alors la vidéo du document principal se lance de 189 à 195 secondes 10 | 11 | Scénario: de texte 12 | 13 | Soit "Treignes, le 8 septembre 2012 (Christophe Lejeune)" le document principal 14 | Et "Etage suivant (Christophe Lejeune)" la glose ouverte 15 | Quand je survole le texte : 16 | """ 17 | Se socialiser 18 | """ 19 | Alors le texte du document principal est en surbrillance : 20 | """ 21 | plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue 22 | """ 23 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography05.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "8b6367c9171b8b1e44625e035c0062aa", 3 | "links": [ 4 | { 5 | "verb": "refersTo", 6 | "object": "6b56ee657c870dfacd34e9ae4e0643dd" 7 | } 8 | ], 9 | "text": "{2} [plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue]\nSe socialiser\n\n[Un autre m'indique que j'aurais intérêt à commencer par les parties hautes de la locomotive, plutôt que des côtés, car celles-ci deviendront vite très chaudes (je risque de me bruler si j'attends pour les laver)]\nDonner son avis sur une tâche en cours\n\n{4} [Luc et Leo exposent les résultats des essais. Les différentes personnes présentes discutent des solutions possibles]\nSe concerter (lors de la pause de midi)\n\n", 10 | "dc_license": "https://creativecommons.org/licenses/by/4.0/", 11 | "dc_issued": "2012", 12 | "dc_language": "french", 13 | "dc_title": "Etage suivant", 14 | "dc_creator": "Christophe Lejeune" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentNotFound.jsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col, Button} from 'react-bootstrap'; 2 | import { useNavigate } from 'react-router'; 3 | 4 | function DocumentNotFound() { 5 | const navigate = useNavigate(); 6 | 7 | return ( 8 | 9 | 10 | 11 |

Document introuvable

12 |

13 | Le document que vous cherchez a été supprimé par les co-éditeurs ou l'URI fournie est incorrecte. 14 |

15 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default DocumentNotFound; 29 | -------------------------------------------------------------------------------- /frontend/src/components/CroppedImage.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/CroppedImage.css'; 2 | 3 | import ReactCrop from 'react-image-crop'; 4 | import 'react-image-crop/dist/ReactCrop.css'; 5 | 6 | function CroppedImage({ src, alt, title }) { 7 | 8 | const image = ; 9 | const caption =
{title}
; 10 | 11 | let fragment = src.match(/#xywh=percent:([.\d]+),([.\d]+),([.\d]+),([.\d]+)/); 12 | if (fragment) { 13 | let [_, x, y, width, height] = fragment; 14 | let crop = { unit: '%', x, y, width, height }; 15 | return ( 16 |
17 | 18 | {image} 19 | 20 | {caption} 21 |
22 | ); 23 | } 24 | return ( 25 |
26 | {image} 27 | {caption} 28 |
29 | ); 30 | } 31 | 32 | export default CroppedImage; 33 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky06.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "b7b35d90733011edb3e7ebf87af5c301", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{6} – Nahát! – mondta az anya –, oda kell küldenem a [másik] lányomat. Gyere csak, Franchon, nézd, mi potyog a húgod szájából beszéd közben: ugye örülnél, ha te is ilyen ajándékban részesülnél? Elég, ha elmész a forráshoz vízért, és ha egy szegény asszony inni kér, adsz neki jó szívvel.\n– Még hogy a forráshoz menjek, eszemben sincs! – felelte a goromba lány.\n– Igenis odamész, mégpedig most rögtön! – ripakodott rá az anya.\nA nagyobbik lány elindult, de egyfolytában dúlt-fúlt. A ház legszebb ezüstkancsóját vitte magával. Alighogy odaért a forráshoz, fenséges ruhába öltözött hölgy lépett ki az erdőből, és inni kért tőle: ugyanaz a tündér volt, de most hercegnő alakját és öltözékét vette magára, hogy lássa, mennyire szívtelen ez a lány." 9 | } 10 | -------------------------------------------------------------------------------- /frontend/scenarios/set_type.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Qualifier un document 4 | 5 | Scénario: non typé avec un type pré-existant 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand je choisis "interview" comme type de glose 10 | Alors le type "Ethnography/Interview" est le type de la glose 11 | 12 | Scénario: typé avec un autre type pré-existant 13 | 14 | Soit un document dont je suis l'auteur affiché comme glose et dont le type est "Ethnography/Report" 15 | Et une session active avec mon compte 16 | Quand je choisis "interview" comme type de glose 17 | Alors le type "Ethnography/Interview" est le type de la glose 18 | 19 | 20 | Scénario: typé comme non typé 21 | 22 | Soit un document dont je suis l'auteur affiché comme glose et dont le type est "Ethnography/Interview" 23 | Et une session active avec mon compte 24 | Quand je choisis "remove current type" comme type de glose 25 | Alors la glose n'a pas de type 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/menu-items/PictureUploadAction.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 4 | 5 | function PictureUploadAction ({ id, backend, handleImageUrl }) { 6 | const fileInputRef = useRef(null); 7 | 8 | const handleClick = () => { 9 | fileInputRef.current.click(); 10 | }; 11 | 12 | const handleFileChange = (event) => { 13 | const file = event.target.files[0]; 14 | if (file) backend.putAttachment(id, file, (response) => { 15 | handleImageUrl(`![](${response.url})`); 16 | }); 17 | }; 18 | 19 | return ( 20 | <> 21 | 22 | Add a picture... 23 | 24 | 31 | 32 | ); 33 | } 34 | 35 | export default PictureUploadAction; 36 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_B.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "564cf7f485c411edba2c6fe0f6ec7a8f", 3 | "dc_title": "Préparation de l'entretien", 4 | "dc_isPartOf": "Archéologie préventive (IF14)", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2019-09-10T15:14:40.577Z", 7 | "text": "- Depuis la réforme de 2003, de nombreux opérateurs ont le droit de faire de l'archéologie préventive. Cependant,nous préfèrons faire notre entretien à l'**INRAP**, car il est national, c'est le plus ancien et la plus important.\n- Le métier le plus représentatif est probablement celui de fouilleur, mais nous décidons d'interviewer un **responsable d'opération**, car, tout en restant proche de la fouille, il semble occuper un rôle clef dans la production des documents produits par l'INRAP.", 8 | "links": [{ 9 | "verb": "refersTo", 10 | "object": "f413206cef01d9b99ebb3b6de2007ca7" 11 | }], 12 | "dc_language": "french", 13 | "dc_license": "https://creativecommons.org/licenses/by/4.0/", 14 | "dc_source": "https://cassandre.utt.fr/memo/f413206cef01d9b99ebb3b6de200c738" 15 | } 16 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova06.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "da6706cc6c1711edbea687297d076097", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{6} — Isto tam musím poslať svoju dcéru, povedala si matka.\nPozri, Fanka, vidíš, čo vychádza z úst tvojej sestry, keď rozpráva? Nepáčilo by sa ti, keby si tiež mala taký dar?\nStačí, keď pôjdeš k studni nabrať vodu, a keď ti nejaká biedna žena povie, že má smäd, láskavo jej dáš napiť.\n— Určite sa mi chce ísť k studni, hrubo odvrkla nemilá dievčina.\n— Chcem, aby si tam išla, rozkázala matka, a to hneď teraz.\nIšla teda, ale stále hundrala. Vzala z domu najkrajšiu striebornú čašu. Len čo prišla k studni, uvidela vychádzať z lesa nádherne oblečenú dámu. Tá k nej podišla a požiadala ju, aby jej dala napiť - bola to tá istá víla, ktorá sa zjavila aj jej sestre, ale teraz sa tvárila a bola oblečená ako princezná. Chcela zistiť, kam až môže zájsť neokrôchanosť dievčaťa." 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import eslint from 'vite-plugin-eslint'; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), eslint({lintOnStart: true})], 7 | define: { 8 | // https://github.com/vitejs/vite/issues/1973#issuecomment-787571499 9 | 'process.env': {}, 10 | }, 11 | server: { 12 | port: 3000, 13 | proxy: { 14 | '/api/_users': { 15 | target: 'http://localhost:5984/_users', 16 | changeOrigin: true, 17 | rewrite: (path) => path.replace(/^\/api\/_users/, ''), 18 | }, 19 | '/api/_session': { 20 | target: 'http://localhost:5984/_session', 21 | changeOrigin: true, 22 | rewrite: (path) => path.replace(/^\/api\/_session/, ''), 23 | }, 24 | '/api': { 25 | target: 'http://localhost:5984/hyperglosae', 26 | changeOrigin: true, 27 | rewrite: (path) => path.replace(/^\/api/, ''), 28 | } 29 | } 30 | }, 31 | build: { 32 | outDir: 'build', 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | 10 | test: 11 | uses: ./.github/workflows/tests.yml 12 | secrets: inherit 13 | 14 | publish: 15 | needs: test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Download sources 19 | uses: actions/checkout@v4 20 | - name: Get frontend build 21 | uses: actions/download-artifact@v4 22 | with: 23 | name: frontend-build 24 | path: frontend/build 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v3 31 | with: 32 | username: benel 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | - name: Build and push 35 | uses: docker/build-push-action@v6 36 | with: 37 | context: . #No checkout 38 | push: true 39 | tags: benel/hyperglosae:latest 40 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_E_content01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "5f08554085cf11edba7403b7c72ddf8c", 3 | "isPartOf": "c2b9f52285ce11edbd0aff9b25defbab", 4 | "text": "## Flux\nParmi les flux évoqués par l'informateur, nous remarquons une grande diversité :\n- de [vestiges],\n- de documents.\n\nNous pouvons faire l'hypothèse que pour contrecarrer son caractère \"destructif\" (étudier une couche stratigraphique nécessite de détruire la couche au-dessus), **l'archéologie transforme les vestiges en documents** ([photographies], plans, [coupes], etc.). Nous essaierons d'en rendre compte dans notre diagramme de flux.\n\nConcernant les vestiges, nous voyons une grande finesse dans la classification de ces derniers (par datation, nature, forme, etc.). Il semblerait même que cette classification (ou \"**typologie**\") soit une part importante du travail des archéologues. Aussi, nous y consacrerons un diagramme de classes.\n", 5 | "dc_source": "https://cassandre.utt.fr/memo/edbb69a33827773d728069d8755133ab", 6 | "links": [{ 7 | "verb": "refersTo", 8 | "object": "6327c5008d1f11ed9aa8e7ae771dee2e" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/scenarios/link_document.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Relier un document existant 4 | 5 | Scénario: pour commenter un document 6 | 7 | Soit un document existant affiché comme document principal 8 | Et une session active avec mon compte 9 | Quand je réutilise "Glossaire" comme glose 10 | Alors "Glossaire" est la glose ouverte 11 | Et la glose contient : 12 | """ 13 | "Il était une fois" 14 | "Once upon a time" (eng) 15 | "Bolo to raz" (svk) 16 | """ 17 | 18 | Scénario: pour le comparer avec un autre 19 | 20 | Soit "Les fées (Charles Perrault)" le document principal 21 | Et avec un document reconnaissable dont je suis l'auteur affiché comme glose en tant que "Quotation" 22 | Soit "A tündérek (Charles Perrault)" le document principal 23 | Et une session active avec mon compte 24 | Quand je réutilise mon document reconnaissable en tant que glose de citation et que je me focalise dessus 25 | Alors la colonne 1 contient "Il était une fois une veuve" 26 | Et la colonne 2 contient "Volt egyszer egy özvegyasszony" 27 | Et la colonne 2 contient "MÁSIK TANULSÁG" 28 | 29 | -------------------------------------------------------------------------------- /frontend/scenarios/edit_passage.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Essayer d'éditer l'annotation d'un passage 4 | 5 | Contexte: 6 | 7 | Soit un document en deux passages affiché comme document principal 8 | Et une glose dont je suis l'auteur faisant référence uniquement au premier passage 9 | Et une session active avec mon compte 10 | 11 | Scénario: quand celle-ci est inexistante 12 | 13 | Quand j'essaie de remplacer l'annotation du passage 2 par : 14 | """ 15 | Passage intéressant ! 16 | """ 17 | Alors la glose contient "Passage intéressant !" 18 | 19 | Scénario: quand celle-ci pré-existe 20 | 21 | Quand j'essaie de remplacer l'annotation du passage 1 par : 22 | """ 23 | Passage intéressant ! 24 | """ 25 | Alors la glose contient "Passage intéressant !" 26 | 27 | Scénario: avec une mise en forme 28 | 29 | Quand j'essaie de remplacer l'annotation du passage 2 par : 30 | """ 31 | [plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue] 32 | Se **socialiser** 33 | """ 34 | Alors la glose contient "Se socialiser" 35 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_jamborova07.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "3d2852666c1811edbfec5bb4e1ae9ba1", 3 | "isPartOf": "420ab198674f11eda3b7a3fdd5ea984f", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "editors": ["alice"], 9 | "text": "{7} — A čo som sem prišla, aby som vám dala napiť? povedala jej nevľúdne namyslená staršia sestra.\nUrčite som priniesla striebornú čašu, na to, aby som dala panej napiť! Viete čo? Napite sa ako chcete!\n— Nie si príliš slušná, povedala víla a ani sa pri tom nerozčúlila.\nKeďže si taká úslužná, dávam ti ako dar, aby ti pri každom slove, ktoré vyslovíš, vyšiel z úst had alebo ropucha.\nLen čo ju matka zbadala, zavolala na ňu:\n— No ako, dcéra moja!?\n— Nuž takto, matka moja, odvetila jej nevľúdne staršia dcéra a vypľula dve zmije a dve ropuchy.\n— Och, Bože! zvolala matka, čo to len vidím? Za to môže tvoja sestra, nedarujem jej to! a okamžite ju bežala zmlátiť. Chúďa dievča sa utieklo skryť do bezpečia v blízkom lese. Tam ju stretol kráľov syn, ktorý sa vracal z poľovačky. Veľmi sa mu zapáčila a tak sa jej spýtal, čo tam robí a prečo plače.\n— Žiaľ, Pane, matka ma vyhnala z domu." 10 | } 11 | -------------------------------------------------------------------------------- /settings/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 512 3 | spread-checks 5 4 | 5 | defaults 6 | mode http 7 | log global 8 | monitor-uri /_haproxy_health_check 9 | option log-health-checks 10 | option httplog 11 | balance roundrobin 12 | option forwardfor 13 | option redispatch 14 | retries 4 15 | option http-server-close 16 | timeout client 150000 17 | timeout server 3600000 18 | timeout connect 500 19 | 20 | stats enable 21 | stats uri /_haproxy_stats 22 | # stats auth admin:admin # Uncomment for basic auth 23 | 24 | frontend http-in 25 | # bind *:$HAPROXY_PORT 26 | bind :80 27 | use_backend couchdb if { path /api } || { path_beg /api/ } 28 | default_backend nginx 29 | 30 | backend couchdb 31 | option httpchk GET /_up 32 | http-check disable-on-404 33 | http-request replace-path /api/_session /_session 34 | http-request replace-path /api/_users/(.*) /_users/\1 35 | http-request replace-path /api(/)?(.*) /hyperglosae/\2 36 | server couchdb1 backend:5984 check inter 5s 37 | 38 | backend nginx 39 | option httpchk HEAD / 40 | server nginx1 frontend:80 check inter 5s 41 | 42 | -------------------------------------------------------------------------------- /frontend/scenarios/open_uri.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Essayer d'ouvrir l'URI partagée 4 | 5 | Scénario: d'un document 6 | 7 | Quand j'essaie d'ouvrir l'URI "/c2b9f52285ce11edbd0aff9b25defbab" reçue par courriel 8 | Alors je ne peux pas lire "Le document que vous cherchez a été supprimé par les co-éditeurs ou l'URI fournie est incorrecte" 9 | Et "Analyse de l'entretien" est le document principal 10 | Et "Vestiges (diagramme de classes)" une des gloses 11 | Et "Flux de l'Institut (diagramme d'activité)" une des gloses 12 | Et "Fouille sur le terrain (diagramme d'activité)" une des gloses 13 | 14 | Scénario: d'un document en regard d'une de ses gloses 15 | 16 | Quand j'essaie d'ouvrir l'URI "/c2b9f52285ce11edbd0aff9b25defbab#146e6e8442f0405b721b79357d00d0a1" reçue par courriel 17 | Alors "Analyse de l'entretien" est le document principal 18 | Et "Flux de l'Institut (diagramme d'activité)" est la glose ouverte 19 | 20 | Scénario: d'un document avec une URI invalide 21 | 22 | Quand j'essaie d'ouvrir l'URI "/document-inexistant#glose-inexistante" reçue par courriel 23 | Alors je peux lire "Le document que vous cherchez a été supprimé par les co-éditeurs ou l'URI fournie est incorrecte" 24 | -------------------------------------------------------------------------------- /samples/hyperglosae/perrault_lorinszky07.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "3cdc7254-7331-11ed-9ca0-a300ea2f2841", 3 | "isPartOf": "09c906c6732b11ed89466ba197585f87", 4 | "links": [{ 5 | "verb": "adapts", 6 | "object": "37b4b9ba5cdb11ed887beb5c373fa643" 7 | }], 8 | "text": "{7} – Hát azért jöttem én ide, hogy inni adjak őnagyságának? – kérdezte ez a kevély, goromba lány. – Hogyne, pont ezért hoztam ezüstkancsót, mi másért; ott a forrás, igyon belőle, ha akar.\n– De kemény a szíved! – felelte a tündér, de nem volt a hangjában harag. – Nos hát, mivel szívélyességnek nyoma sincs benned, azt adom neked ajándékul, hogy valahányszor megszólalsz, kígyó vagy varangy fog kibújni a szádból.\nAmikor a lányt meglátta az anyja, már messziről odakiáltott:\n– No, mi van, lányom?\n– Mi lenne, anyám? – felelte a goromba lány, és közben két vipera és két varangy bújt ki a szájából.\n– Ó, egek, mit látok? – sikoltott az anya. – Ez a húgod bűne; de meg is kapja érte a magáét!\nMár szaladt is, hogy megverje. A szegény gyermek elszökött, és a közeli erdőben keresett menedéket. A vadászatról visszatérő királyfi ott találta, és látva, milyen szép, megkérdezte tőle, mit csinál ott egymaga, és miért sír.\n– Jaj, uram, anyám üldözött el otthonról." 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | backend: 4 | extends: 5 | file: docker-compose.dev.yml 6 | service: backend 7 | 8 | updated_samples: 9 | extends: 10 | file: docker-compose.dev.yml 11 | service: updated_samples 12 | depends_on: 13 | backend: 14 | condition: service_healthy 15 | 16 | updated_code: 17 | extends: 18 | file: docker-compose.dev.yml 19 | service: updated_code 20 | depends_on: 21 | backend: 22 | condition: service_healthy 23 | 24 | frontend: 25 | image: nginx 26 | volumes: 27 | - ./frontend/build:/usr/share/nginx/html:ro 28 | - ./settings/nginx.conf:/etc/nginx/conf.d/default.conf:ro 29 | healthcheck: 30 | test: curl --fail --head localhost || exit 1 31 | start_period: 5s 32 | start_interval: 2s 33 | depends_on: 34 | updated_code: 35 | condition: service_completed_successfully 36 | 37 | proxy: 38 | image: haproxy 39 | ports: 40 | - 80:80 41 | volumes: 42 | - ./settings:/usr/local/etc/haproxy:ro 43 | depends_on: 44 | backend: 45 | condition: service_healthy 46 | frontend: 47 | condition: service_healthy 48 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/FragmentComment.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/FragmentComment.css'; 2 | 3 | function FragmentComment({ children, setHighlightedText }) { 4 | try { 5 | children = (children instanceof Array) ? children : [children]; 6 | const citationRegex = /^\[.*\]\s*\n(.*)$/m; 7 | if (citationRegex.test(children[0])) { 8 | let [citation, comment] = children[0].split(/\n/); 9 | citation = citation.replace(/[[\]]/g, ''); 10 | const commentParts = [ 11 | comment, 12 | ...children.slice(1) 13 | ].map((part, index) => ( 14 | 15 | {part} 16 | 17 | )); 18 | 19 | return

{ 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | setHighlightedText(citation); 25 | } 26 | } 27 | onMouseLeave={ 28 | e => { 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | setHighlightedText(''); 32 | } 33 | } 34 | className="fragment" 35 | > 36 | {citation} 37 | {commentParts} 38 |

; 39 | } 40 | } catch (e) { 41 | console.error(e); 42 | } 43 | } 44 | 45 | export default FragmentComment; 46 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentsCards.jsx: -------------------------------------------------------------------------------- 1 | import Card from 'react-bootstrap/Card'; 2 | import Col from 'react-bootstrap/Col'; 3 | import Row from 'react-bootstrap/Row'; 4 | import Metadata from './Metadata'; 5 | import BrowseTools from './BrowseTools'; 6 | import FutureDocument from './FutureDocument'; 7 | import { TypeBadge } from './Type'; 8 | 9 | function DocumentsCards({docs, expandable, byRow, createOn, setLastUpdate, backend, user}) { 10 | return ( 11 | 12 | {docs.map(x => x?._id && x?.dc_title && 13 | 14 | 15 | 16 | )} 17 | {createOn && 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | } 26 | 27 | ); 28 | } 29 | 30 | function DocumentCard({doc, expandable}) { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export default DocumentsCards; 43 | -------------------------------------------------------------------------------- /frontend/src/menu-items/DeleteDocumentAction.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | import { Modal, Button } from 'react-bootstrap'; 4 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 5 | 6 | function DeleteDocumentAction({metadata, isFromScratch, backend, setLastUpdate}) { 7 | const navigate = useNavigate(); 8 | const [show, setShow] = useState(false); 9 | 10 | const handleClick = () => { 11 | backend.deleteDocument(metadata) 12 | .then(x => setLastUpdate(x.rev)) 13 | .then(() => navigate(isFromScratch ? '/' : '#')); 14 | }; 15 | 16 | return ( 17 | <> 18 | setShow(true)}> 19 | Delete... 20 | 21 | setShow(false)}> 22 | 23 | Are you sure you want to delete this document (with all of its contents and metadata). 24 | This action cannot be undone. 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default DeleteDocumentAction; 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Acceptance tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - main 6 | workflow_call: 7 | 8 | jobs: 9 | 10 | build: 11 | uses: ./.github/workflows/build.yml 12 | 13 | functional: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Download sources 17 | uses: actions/checkout@v4 18 | - name: Download tools 19 | run: | 20 | cd frontend; npm install --save-dev @badeball/cypress-cucumber-preprocessor 21 | - name: Install and launch application 22 | uses: ./.github/actions/launch 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Run tests 26 | run: | 27 | cd frontend; npx cypress run --config baseUrl=http://localhost 28 | 29 | footprint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Download sources 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 # Greenframe needs the whole history 36 | - name: Download tools 37 | run: | 38 | curl https://assets.greenframe.io/install.sh | bash 39 | - name: Install and launch application 40 | uses: ./.github/actions/launch 41 | with: 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | - name: Measure carbon footprint 44 | run: | 45 | cd requirements 46 | greenframe analyze >> $GITHUB_STEP_SUMMARY 47 | 48 | -------------------------------------------------------------------------------- /.github/actions/launch/action.yml: -------------------------------------------------------------------------------- 1 | name: Launch Hyperglosae 2 | description: Install and launch both backend and frontend (sources should be checked out). 3 | 4 | inputs: 5 | repo-token: 6 | description: GH token to access frontend build 7 | required: true 8 | 9 | runs: 10 | using: composite 11 | steps : 12 | - name: Download, install and launch backend with test data 13 | run: | 14 | export COUCHDB_USER="TO_BE_CHANGED" 15 | export COUCHDB_PASSWORD="TO_BE_CHANGED" 16 | docker compose --file docker-compose.test.yml up --detach updated_samples updated_code 17 | shell: bash 18 | - name: Wait for frontend build 19 | uses: lewagon/wait-on-check-action@v1.3.1 20 | with: 21 | check-regexp: .? / build 22 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 23 | repo-token: ${{ inputs.repo-token }} 24 | - name: Get frontend build 25 | uses: actions/download-artifact@v4 26 | with: 27 | name: frontend-build 28 | path: frontend/build 29 | - name: Start frontend 30 | run: | 31 | export COUCHDB_USER="TO_BE_CHANGED" 32 | export COUCHDB_PASSWORD="TO_BE_CHANGED" 33 | docker compose --file docker-compose.test.yml up --detach 34 | shell: bash 35 | - name: Wait for frontend 36 | uses: docker://benel/wait-for-response:1 37 | with: 38 | args: http://localhost/ 200 30000 500 39 | 40 | -------------------------------------------------------------------------------- /frontend/scenarios/edit_metadata.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Essayer d'éditer les métadonnées d'une glose 4 | 5 | Scénario: dont on est l'auteur 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand j'essaie de remplacer les métadonnées de la glose par : 10 | """ 11 | dc_title: Commentaire 12 | dc_creator: Alice Liddell 13 | dc_issued: 1932 14 | dc_language: french 15 | dc_translator: Charles Beaudelaire 16 | """ 17 | Alors "Commentaire" est la glose ouverte 18 | Et le créateur est "Alice Liddell" 19 | Et l'année de publication est "1932" 20 | Et la langue est "French" 21 | 22 | Scénario: dont on n'est pas l'auteur 23 | 24 | Soit un document dont je ne suis pas l'auteur affiché comme glose 25 | Et une session active avec mon compte 26 | Quand j'essaie de remplacer les métadonnées de la glose par : 27 | """ 28 | dc_title: Commentaire 29 | dc_creator: Alice Liddell 30 | dc_issued: 1932 31 | """ 32 | Alors je peux lire "Before editing this document, please request authorization to its editors first" 33 | 34 | Scénario: sans être connecté 35 | 36 | Soit un document dont je suis l'auteur affiché comme glose 37 | Quand j'essaie de remplacer les métadonnées de la glose par : 38 | """ 39 | dc_title: Commentaire 40 | dc_creator: Alice Liddell 41 | dc_issued: 1932 42 | """ 43 | Alors je peux lire "Before editing this document, please log in first" 44 | -------------------------------------------------------------------------------- /frontend/src/components/VideoComment.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/VideoComment.css'; 2 | 3 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 4 | 5 | function VideoComment({ children }) { 6 | children = (children instanceof Array) ? children : [children]; 7 | const timecodeRegex = /(\d{2}:\d{2}:\d{2}\.\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}\.\d{3})/; 8 | if (timecodeRegex.test(children)) return ( 9 | Play the video fragment} > 10 |

{ 13 | e.preventDefault(); 14 | e.stopPropagation(); 15 | playVideoAt(children[0]); 16 | } 17 | } 18 | className="videoComment" 19 | > 20 | {children[0]} 21 |

22 |
23 | ); 24 | 25 | function playVideoAt(timecode) { 26 | let [start, end] = timecode.split('-->'); 27 | let [hour, min, sec] = start.split(/[:.]/); 28 | let startTime = Number(hour * 3600) + Number(min * 60) + Number(sec); 29 | [hour, min, sec] = end.split(/[:.]/); 30 | let endTime = Number(hour * 3600) + Number(min * 60) + Number(sec); 31 | let iframe = document.getElementsByTagName('iframe'); 32 | if (iframe.length != 0) { 33 | let youTubeLink = new URL(iframe[0].src); 34 | let youTubeBaseLink = youTubeLink.origin + youTubeLink.pathname; 35 | let targetLink = `${youTubeBaseLink}?start=${startTime}&end=${endTime}&autoplay=1&mute=1`; 36 | iframe[0].src = targetLink; 37 | } 38 | } 39 | } 40 | 41 | export default VideoComment; 42 | -------------------------------------------------------------------------------- /frontend/scenarios/set_license.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Définir la licence 4 | 5 | Contexte: 6 | 7 | Soit "Vidéo Sherlock Jr. (Buster Keaton)" le document principal 8 | Et "All rights reserved" le nom de la licence du document principal 9 | Et une session active avec mon compte 10 | 11 | Scénario: d'une traduction avec licence compatible avec celle de la source 12 | 13 | Et j'essaie de créer une glose de type "Adaptation" 14 | Quand j'essaie de remplacer les métadonnées de la glose par : 15 | """ 16 | dc_license: All rights reserved 17 | """ 18 | Alors le nom de la licence de la glose est "All rights reserved" 19 | Et je ne peux pas lire "Licenses are not compatible" 20 | 21 | Scénario: d'une traduction avec une licence incompatible avec celle de la source 22 | 23 | Et j'essaie de créer une glose de type "Adaptation" 24 | Quand j'essaie de remplacer les métadonnées de la glose par : 25 | """ 26 | dc_license: https://creativecommons.org/licenses/by-sa/4.0/ 27 | """ 28 | Alors le code de la licence de la glose est "CC-BY-SA" 29 | Et je peux lire "Licenses are not compatible" 30 | 31 | Scénario: d'un commentaire avec une licence incompatible avec celle de la source 32 | 33 | Et j'essaie de créer une glose de type "Commentary" 34 | Quand j'essaie de remplacer les métadonnées de la glose par : 35 | """ 36 | dc_license: https://creativecommons.org/licenses/by-sa/4.0/ 37 | """ 38 | Alors le code de la licence de la glose est "CC-BY-SA" 39 | Et je ne peux pas lire "Licenses are not compatible" 40 | 41 | -------------------------------------------------------------------------------- /frontend/scenarios/invite_editor.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Essayer d'inviter quelqu'un à éditer un document 4 | 5 | Scénario: dont on est l'auteur 6 | 7 | Soit un document reconnaissable dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand j'essaie d'accorder les droits d'édition à "Christophe" 10 | Alors je vois "christophe" dans la liste des éditeurs 11 | Et "christophe" peut modifier le document 12 | Et le document apparaît dans la bibliothèque de "christophe" 13 | 14 | Scénario: dont on n'est pas l'auteur 15 | 16 | Soit un document dont je ne suis pas l'auteur affiché comme glose 17 | Et une session active avec mon compte 18 | Quand j'essaie d'accorder les droits d'édition à "christophe" 19 | Alors je peux lire "Before editing this document, please request authorization to its editors first" 20 | 21 | Scénario: sans être connecté 22 | 23 | Soit un document reconnaissable dont je suis l'auteur affiché comme glose 24 | Quand j'essaie d'accorder les droits d'édition à "christophe" 25 | Alors je peux lire "Before editing this document, please log in first" 26 | 27 | Scénario: en appuyant sur Entrée 28 | 29 | Soit un document reconnaissable dont je suis l'auteur affiché comme glose 30 | Et une session active avec mon compte 31 | Quand j'essaie d'accorder les droits d'édition à "Christophe" en appuyant sur Entrée 32 | Alors je vois "christophe" dans la liste des éditeurs 33 | Et "christophe" peut modifier le document 34 | Et le document apparaît dans la bibliothèque de "christophe" 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/LicenseCompatibility.jsx: -------------------------------------------------------------------------------- 1 | const licenseCompatibility = { 2 | 'Public domain': ['Public domain', 'by', 'by-sa', 'by-nc', 'by-nc-sa', 'by-nc-nd', 'by-nd', 'All rights reserved'], 3 | 'by': ['by', 'by-sa', 'by-nc', 'by-nc-sa', 'by-nc-nd', 'by-nd', 'All rights reserved'], 4 | 'by-sa': ['by-sa'], 5 | 'by-nc': ['by-nc', 'by-nc-sa', 'by-nc-nd'], 6 | 'by-nc-sa': ['by-nc-sa'], 7 | 'by-nc-nd': [], 8 | 'by-nd': [], 9 | 'All rights reserved': ['All rights reserved'], 10 | }; 11 | 12 | function LicenseCompatibility({ sourceMetadata, marginMetadata }) { 13 | const isAdaptation = marginMetadata?.links?.some(x => x.verb === 'adapts'); 14 | 15 | const getLicenseKey = (licenseUri) => { 16 | if (!licenseUri) return 'All rights reserved'; 17 | if (licenseUri.toLowerCase() === 'public domain') return 'Public domain'; 18 | const matches = /BY[\w-]*/i.exec(licenseUri); 19 | return matches ? matches[0].toLowerCase() : 'All rights reserved'; 20 | }; 21 | 22 | const sourceKey = getLicenseKey(sourceMetadata?.dc_license); 23 | const marginKey = getLicenseKey(marginMetadata?.dc_license); 24 | const isCompatible = isAdaptation ? licenseCompatibility[sourceKey]?.includes(marginKey) : true; 25 | 26 | const warningStyle = { 27 | textAlign: 'right' 28 | }; 29 | 30 | return ( 31 | !isCompatible && ( 32 |
33 | Licenses are not compatible 34 |
35 | ) 36 | ); 37 | } 38 | 39 | export default LicenseCompatibility; 40 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentList.jsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'react-bootstrap'; 2 | import { useState, useEffect } from 'react'; 3 | import ExistingDocument from './ExistingDocument'; 4 | 5 | function DocumentList({ relatedTo, verb, setLastUpdate, backend, user }) { 6 | const [userDocuments, setUserDocuments] = useState([]); 7 | const [searchQuery, setSearchQuery] = useState(''); 8 | 9 | const handleSearchChange = (event) => { 10 | setSearchQuery(event.target.value); 11 | }; 12 | 13 | const filteredDocuments = userDocuments.filter(({dc_title}) => 14 | dc_title?.toLowerCase().includes(searchQuery.toLowerCase()) 15 | ); 16 | 17 | useEffect(() => { 18 | backend.getAllDocuments(user) 19 | .then(x => 20 | setUserDocuments( 21 | x.filter(y => y && y._id !== relatedTo[0]) 22 | ) 23 | ); 24 | }, [user, backend, relatedTo]); 25 | 26 | return ( 27 | <> 28 | 29 | 30 | 37 | 38 | 39 |
40 | {filteredDocuments.map(document => ( 41 | 44 | ))} 45 |
46 | 47 | ); 48 | } 49 | 50 | export default DocumentList; 51 | -------------------------------------------------------------------------------- /frontend/src/menu-items/BreakIntoPassagesAction.jsx: -------------------------------------------------------------------------------- 1 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 2 | import { NotificationManager } from 'react-notifications'; 3 | 4 | function BreakIntoPassagesAction({parallelDocuments, margin, backend, setLastUpdate}) { 5 | 6 | let disabled = true; 7 | let scholium; 8 | let firstPassage = parallelDocuments.passages[0]; 9 | 10 | // disabled if margin has already rubrics 11 | if (!parallelDocuments.doesMarginHaveRubrics && firstPassage) { 12 | let scholia = firstPassage.scholia.filter(x => x.isPartOf === margin); 13 | // disabled if different chunks 14 | let hasChunks = scholia.length > 1; 15 | if (!hasChunks) { 16 | scholium = scholia[0]; 17 | // disabled if source has rubrics and margin is not empty 18 | disabled = !scholium 19 | || parallelDocuments.doesSourceHaveRubrics && scholium.text && scholium.text !== '…'; 20 | } 21 | } 22 | 23 | const handleClick = () => { 24 | let text = scholium.text 25 | .split('\n') 26 | .map((x, i) => '{' + (i + 1) + '} ' + x) 27 | .join(''); 28 | backend.getDocument(scholium.id) 29 | .then(x => backend.putDocument({...x, text})) 30 | .then(x => { 31 | setLastUpdate(x.rev); 32 | NotificationManager.success('The text has been successfully split into passages.', '', 2000); 33 | }); 34 | }; 35 | 36 | return ( 37 | 38 | Break into numbered passages 39 | 40 | ); 41 | } 42 | 43 | export default BreakIntoPassagesAction; 44 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/ExistingDocument.jsx: -------------------------------------------------------------------------------- 1 | import Card from 'react-bootstrap/Card'; 2 | import { useNavigate } from 'react-router'; 3 | 4 | function ExistingDocument({ document, relatedTo, verb, setLastUpdate, backend }) { 5 | const navigate = useNavigate(); 6 | const title = document.dc_title || 'Untitled document'; 7 | const sourceChunksToBeLinked = (verb !== 'includes' && relatedTo.length) 8 | ? [{ verb, object: relatedTo[0] }] 9 | : relatedTo.map(object =>({ verb, object })); 10 | 11 | const handleClick = async () => { 12 | const gloseChunksToBeLinked = (verb === 'includes') 13 | ? Promise.resolve([document._id]) 14 | : backend.getView({view: 'content', id: document._id}) 15 | .then(rows => rows 16 | .filter(x => x.value.isPartOf === document._id) 17 | .map(x => x.id) 18 | .reduce((set, item) => set.includes(item) ? set : [...set, item], [document._id]) 19 | ); 20 | gloseChunksToBeLinked.then(l => 21 | l.map(x => 22 | backend.getDocument(x) 23 | .then(chunk => backend.putDocument({ 24 | ...chunk, 25 | links: [...chunk.links || [], ...sourceChunksToBeLinked] 26 | })) 27 | ) 28 | ) 29 | .then(x => Promise.all(x)) 30 | .then(() => { 31 | setLastUpdate(document._id); 32 | navigate('#' + document._id); 33 | }) 34 | .catch(console.error); 35 | }; 36 | 37 | return ( 38 | 39 | 40 | {title} 41 | 42 | 43 | ); 44 | } 45 | 46 | export default ExistingDocument; 47 | -------------------------------------------------------------------------------- /frontend/src/styles/Lectern.css: -------------------------------------------------------------------------------- 1 | .lectern { 2 | display: flex; 3 | flex-direction: column; 4 | text-align: justify; 5 | border-radius: 10px; 6 | } 7 | 8 | .license-container { 9 | display: flex; 10 | justify-content: flex-end; 11 | margin-top: auto; 12 | } 13 | 14 | .lectern .row p { 15 | white-space: pre-wrap; 16 | } 17 | 18 | .main { 19 | background-color: ghostwhite; 20 | } 21 | 22 | .lectern .runningHead > div { 23 | padding-top: 17px; 24 | padding-bottom: 17px; 25 | } 26 | 27 | .lectern .row:first-of-type .main { 28 | border-radius: 10px 0px 0px 0px; 29 | text-align: left; 30 | } 31 | 32 | .lectern .row:last-of-type .main { 33 | border-radius: 0px 0px 0px 10px; 34 | } 35 | 36 | .rubric { 37 | color: crimson; 38 | text-align: center; 39 | } 40 | 41 | .scholium { 42 | background-color: gainsboro; 43 | } 44 | 45 | .lectern .row:first-of-type .scholium { 46 | border-radius: 0px 10px 0px 0px; 47 | justify-content: flex-end; 48 | text-align: right; 49 | } 50 | 51 | .lectern .row:last-of-type .scholium { 52 | border-radius: 0px 0px 10px 0px; 53 | } 54 | 55 | .lectern > .row:last-of-type > div { 56 | padding-bottom: 17px; 57 | } 58 | 59 | .runningHead { 60 | color: crimson; 61 | } 62 | 63 | .icon, 64 | .icon:hover { 65 | color: crimson; 66 | text-decoration: none; 67 | margin-right: .5rem; 68 | } 69 | 70 | .sources .card, 71 | .gloses .card { 72 | background-color: gainsboro; 73 | color: black; 74 | } 75 | 76 | p, ul, h1, h2 { 77 | margin-bottom: 0px !important; 78 | } 79 | 80 | dd { 81 | padding-left: 30px; 82 | } 83 | 84 | .icon.bookmarked { 85 | color: gold; /* or any color that indicates an active bookmark */ 86 | } 87 | 88 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "bootstrap": "^5.2.2", 8 | "d3": "^7.9.0", 9 | "d3-svg-legend": "^2.25.6", 10 | "date-fns": "^4.1.0", 11 | "events": "^3.3.0", 12 | "react": "^18.2.0", 13 | "react-bootstrap": "^2.5.0", 14 | "react-bootstrap-icons": "^1.10.2", 15 | "react-dom": "^18.2.0", 16 | "react-image-crop": "^11.0.7", 17 | "react-mark.js": "^10.0.10", 18 | "react-markdown": "^10.1.0", 19 | "react-notifications": "^1.7.4", 20 | "react-router": "^7.10.1", 21 | "remark-definition-list": "^2.0.0", 22 | "remark-gfm": "^4.0.1", 23 | "remark-unwrap-images": "^4.0.0", 24 | "uuid": "^13.0.0", 25 | "vite-plugin-eslint": "^1.8.1", 26 | "yaml": "^2.2.1" 27 | }, 28 | "scripts": { 29 | "start": "vite", 30 | "lint": "eslint .", 31 | "test": "cypress run", 32 | "test2": "cypress open", 33 | "weigh": "vite-bundle-visualizer", 34 | "build": "vite build" 35 | }, 36 | "cypress-cucumber-preprocessor": { 37 | "stepDefinitions": "tests/{context,event,outcome}.js" 38 | }, 39 | "devDependencies": { 40 | "@badeball/cypress-cucumber-preprocessor": "^24.0.0", 41 | "@bahmutov/cypress-esbuild-preprocessor": "^2.2.4", 42 | "@eslint/js": "^9.9.0", 43 | "@vitejs/plugin-react-swc": "^4.2.2", 44 | "cypress": "^15.7.1", 45 | "eslint": "^9.9.0", 46 | "eslint-plugin-react": "^7.35.0", 47 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 48 | "eslint-plugin-react-refresh": "^0.4.9", 49 | "globals": "^16.3.0", 50 | "vite": "^7.0.6", 51 | "vite-bundle-visualizer": "^1.2.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/scenarios/show_history.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Consulter dans l'historique d'un document 4 | 5 | Scénario: dont je suis l'auteur, l'auteur et la date de la création de ce document 6 | 7 | Soit un document existant affiché comme document principal 8 | Quand je consulte l'historique du document 9 | Alors je peux voir l'auteur de la création du document "alice" et sa date de création 10 | 11 | Scénario: dont je ne suis pas l'auteur, l'auteur et la date de la création de ce document 12 | 13 | Soit un document dont je ne suis pas l'auteur affiché comme document principal 14 | Quand je consulte l'historique du document 15 | Alors je peux voir l'auteur de la création du document "bill" et sa date de création 16 | 17 | Scénario: dont je suis l'auteur, une modification 18 | 19 | Soit un document existant affiché comme document principal 20 | Et une session active avec mon compte 21 | Et que je modifie le document 22 | Quand je consulte l'historique du document 23 | Alors je peux voir une modification effectuée par "alice" et la date de cette modification 24 | 25 | Scénario: dont je ne suis pas l'auteur, une modification 26 | 27 | Soit un document dont je ne suis pas l'auteur affiché comme document principal et qui a parmi ses éditeurs "alice" 28 | Et une session active avec mon compte 29 | Et que je modifie le document 30 | Quand je consulte l'historique du document 31 | Alors je peux voir une modification effectuée par "alice" et la date de cette modification 32 | 33 | Scénario: sans historique 34 | 35 | Soit un document sans champ "history" affiché comme document principal 36 | Quand je consulte l'historique du document 37 | Alors aucun historique n'est affiché 38 | -------------------------------------------------------------------------------- /samples/hyperglosae/inrap_D.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "6327c5008d1f11ed9aa8e7ae771dee2e", 3 | "dc_title": "Étiquetage de l'entretien", 4 | "dc_isPartOf": "Archéologie préventive (IF14)", 5 | "dc_creator": "Aurélien Bénel", 6 | "dc_issued": "2019-09-24", 7 | "links": [{ 8 | "verb": "refersTo", 9 | "object": "05b61f5285c711ed97bf6b9b56808c45" 10 | }], 11 | "dc_language": "french", 12 | "editors": ["benel"], 13 | "text":"{1} [responsable d'opération]\nActeur{5} [fouiller selon le programme]\nAction{7} [recherches historiques sur la zone balisée]\nFlux{9} [coupes]\nFlux\n\n[structures]\nFlux\n\n[tamisage]\nAction\n\n[relever les coupes]\nAction{15} [objets]\nFlux\n\n[sacs]\nFlux\n\n[terre]\nFlux\n\n[relevé topographique]\nFlux\n\n[photos]\nFlux\n\n[creuser]\nAction\n\n[prendre en photo]\nAction\n\n[dessiner les plans]\nAction\n\n[archiver tous les sacs]\nAction\n\n[décapage du terrain]\nAction\n\n[prélever la terre, les objets, les mettre en sac]\nAction{17} [mobilier]\nFlux\n\n[rapport]\nFlux\n\n[fiche d'enregistrement]\nFlux\n\n[interpréter]\nAction\n\n[lavage]\nAction\n\n[scannés et redessinés sur ordinateur]\nAction\n\n{19}[graines]\nFlux\n\n[charbons]\nFlux\n\n[faire des prélèvements]\nAction{21}[reversés aux collections publiques]\nAction\n\n[service régional de l'archéologie]\nActeur\n\n[non les garde pendant deux ans en études]\nAction{31} [vestiges]\nFlux\n\n[parcelle]\nFlux{41} [techniciens]\nActeur{43} [les CDD, ceux qui sont de passage à l'INRAP, souvent des prestataires]\nActeur{45} [publications]\nFlux\n\n[chercheurs]\n Acteur\n\n[responsables d'opérations mais spécialisés sur des périodes chronologiques]\nActeur{53} [fouilleurs]\nActeur", 14 | "dc_license": "https://creativecommons.org/licenses/by/4.0/" 15 | } 16 | -------------------------------------------------------------------------------- /tools/from_cassandre.js: -------------------------------------------------------------------------------- 1 | const ORIGIN = 'http://localhost:5984/cassandre'; 2 | const ORIGIN_CREDENTIALS = 'TO_BE_CHANGED:TO_BE_CHANGED'; 3 | const DESTINATION = 'http://localhost:5984/hyperglosae'; 4 | const DESTINATION_CREDENTIALS = 'TO_BE_CHANGED:TO_BE_CHANGED'; 5 | const DIARY = '27a9c19269be863f31acd74d740cd8c0'; 6 | const EDITORS = ['margaux.coeuret@utt.fr', 'benel']; 7 | 8 | const postJSON = (uri, credentials, object) => 9 | fetch(uri, { 10 | method: 'POST', 11 | body: JSON.stringify(object), 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Authorization': `Basic ${btoa(credentials)}`, 15 | }, 16 | }) 17 | .then(x => x.json()); 18 | 19 | const convertToLink = (grounding) => ({ 20 | verb: 'refersTo', 21 | object: grounding._id || grounding 22 | }); 23 | 24 | const addUserOnce = (users, {user}) => 25 | users.includes(user) ? users : [...users, user]; 26 | 27 | const convertMemo = 28 | ({ 29 | _id, 30 | name, 31 | body, 32 | statement, 33 | type, 34 | groundings = [], 35 | history = [] 36 | }) => ({ 37 | _id, 38 | dc_title: name, 39 | text: body || statement, 40 | type, 41 | links: [...new Set(groundings.map(convertToLink))], 42 | dc_creator: history.reduce(addUserOnce, []), 43 | dc_issued: history[0]?.date, 44 | editors: EDITORS, 45 | }); 46 | 47 | postJSON(`${ORIGIN}/_find`, ORIGIN_CREDENTIALS, { 48 | selector: { 49 | diary: { 50 | $eq: DIARY 51 | } 52 | }, 53 | limit: 1000, 54 | }) 55 | .then(({docs}) => docs.map(convertMemo)) 56 | .then(docs => 57 | postJSON(`${DESTINATION}/_bulk_docs`, DESTINATION_CREDENTIALS, ({ 58 | docs, 59 | })) 60 | ) 61 | .then(console.log); 62 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/BrowseTools.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router'; 2 | import { Bookmark, ChevronBarDown, ChevronExpand, PencilSquare} from 'react-bootstrap-icons'; 3 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 4 | 5 | function BrowseTools({id, closable, openable, editable, focusable = true}) { 6 | return ( 7 | <> 8 | {editable && 9 | Edit this document} 12 | > 13 | 14 | 15 | 16 | 17 | } 18 | 19 | {closable && 20 | Close this document} 23 | > 24 | 25 | 26 | 27 | 28 | } 29 | 30 | {openable && 31 | Open this document} 34 | > 35 | 36 | 37 | 38 | 39 | } 40 | 41 | {focusable && 42 | Focus on this document} 45 | > 46 | 47 | 48 | 49 | 50 | } 51 | 52 | ); 53 | } 54 | 55 | export default BrowseTools; 56 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, StrictMode } from 'react'; 2 | import './styles/index.css'; 3 | import Menu from './components/Menu'; 4 | import Lectern from './routes/Lectern'; 5 | import Bookshelf from './routes/Bookshelf'; 6 | import Hyperglosae from './hyperglosae'; 7 | import 'bootstrap/dist/css/bootstrap.min.css'; 8 | import { BrowserRouter, Routes, Route } from 'react-router'; 9 | import { NotificationContainer, NotificationManager } from 'react-notifications'; 10 | import 'react-notifications/lib/notifications.css'; 11 | import { TypesContext } from './components/TypesContext.js'; 12 | import Registration from './routes/Registration'; 13 | 14 | const backend = new Hyperglosae( 15 | x => NotificationManager.warning(x, '', 2000) 16 | ); 17 | 18 | function App() { 19 | const [types, setTypes] = useState([]); 20 | const [user, setUser] = useState(); 21 | useEffect(() => { 22 | backend.getView({view: 'types', options: ['include_docs']}) 23 | .then( 24 | (rows) => { 25 | setTypes(rows); 26 | } 27 | ); 28 | backend.getSession() 29 | .then(setUser); 30 | }, []); 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | } /> 39 | } /> 40 | } /> 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | export default App; 49 | 50 | -------------------------------------------------------------------------------- /samples/hyperglosae/cinema_script.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "4e1a31e14b032f2fa9e161ee9b009122", 3 | "dc_title": "Script Buster Keaton", 4 | "dc_creator": "Astrid De Véricourt", 5 | "dc_issued": "2023-02-21", 6 | "text": "Exemple de suite d'extrait\n## Titre : Sherlock Jr.\n\n**Scène 1 :** Le rêve de Sherlock Jr.\nSherlock Jr. est un projectionniste de cinéma qui aspire à devenir un grand détective privé. Il s'endort dans la cabine de projection et rêve qu'il est en train de résoudre un mystère compliqué.\n\n**Scène 2 :** Le vol du médaillon\nSherlock Jr. est réveillé par un client qui lui demande de retrouver un médaillon volé. Le client soupçonne la petite amie de Sherlock Jr. d'avoir commis le vol. Sherlock Jr. refuse d'accepter l'affaire, mais son rival, le détective Brent, prend le cas à sa place.\n\n**Scène 3 :** La poursuite\nSherlock Jr. est déterminé à prouver l'innocence de sa petite amie et décide de mener sa propre enquête. Il suit le véritable voleur, un homme mystérieux, et entreprend une poursuite dangereuse à travers la ville. Le voleur, ayant remarqué qu'il est suivi, commence à semer des pièges sur le chemin de Sherlock Jr. qui doit les éviter habilement.\n\n**Scène 4 :** La confrontation finale\nLa poursuite se termine dans une usine abandonnée où le voleur a pris refuge. Sherlock Jr. le confronte et découvre que le véritable coupable est en fait le détective Brent, le rival jaloux qui voulait s'attribuer la résolution de l'affaire. Brent révèle que c'était un plan diabolique pour salir la réputation de la petite amie de Sherlock Jr. et l'empêcher de devenir un détective privé renommé.", 7 | "dc_license": "https://creativecommons.org/licenses/by-sa/4.0/", 8 | "links": [{ 9 | "verb": "adapts", 10 | "object": "4e1a31e14b032f2fa9e161ee9b009125" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/Bookmark.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { BookmarkFill } from 'react-bootstrap-icons'; 3 | import { v4 as uuid } from 'uuid'; 4 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 5 | 6 | function Bookmark({backend, user, id}) { 7 | const [isBookmarked, setIsBookmarked] = useState(false); 8 | 9 | const getBookmark = useCallback((id, user) => 10 | backend.getView({view: 'bookmark', id: user, options: ['include_docs']}) 11 | .then(rows => rows.find(row => row.doc.bookmark === id)), 12 | [backend]); 13 | 14 | useEffect(() => { 15 | if (user) { 16 | getBookmark(id, user) 17 | .then(bookmark => setIsBookmarked(!!bookmark)) 18 | .catch(console.error); 19 | } 20 | }, [user, id, getBookmark]); 21 | 22 | const onBookmarkToggle = () => { 23 | if (!isBookmarked) { 24 | backend.putDocument({ _id: uuid(), editors: [user], bookmark: id }) 25 | .then(() => setIsBookmarked(true)) 26 | .catch(console.error); 27 | } else { 28 | getBookmark(id, user) 29 | .then(x => x.doc) 30 | .then(backend.deleteDocument) 31 | .then(() => setIsBookmarked(false)) 32 | .catch(console.error); 33 | } 34 | }; 35 | 36 | return ( 37 | 41 | {isBookmarked 42 | ? 'Remove this document from your bookshelf' 43 | : 'Add this document to your bookshelf'} 44 | 45 | } 46 | > 47 | 52 | 53 | ); 54 | } 55 | 56 | export default Bookmark; 57 | -------------------------------------------------------------------------------- /frontend/src/routes/Registration.jsx: -------------------------------------------------------------------------------- 1 | import Form from 'react-bootstrap/Form'; 2 | import Button from 'react-bootstrap/Button'; 3 | import { NotificationManager } from 'react-notifications'; 4 | 5 | function Registration({backend}) { 6 | 7 | const handleSubmit = (e) => { 8 | e.preventDefault(); 9 | let user = { 10 | name: e.target[0].value.toLowerCase(), 11 | password: e.target[1].value, 12 | type: 'user', 13 | roles: [], 14 | created: new Date() 15 | }; 16 | backend.putDocument(user, `_users/org.couchdb.user:${user.name}`) 17 | .then(() => { 18 | NotificationManager.success(`${user.name} is now registered!`); 19 | e.target.reset(); 20 | }); 21 | }; 22 | 23 | return ( 24 |
25 | 26 | Email address 27 | 28 | 29 |

Please use your e-mail address given by your organization 30 | (university company, NGO...).

31 |

Do not use addresses created anonymously (such as GMail, Yahoo...).

32 |
33 |
34 | 35 | 36 | Password 37 | 38 | 39 |

Set a new password, a password you never used anywhere else.

40 |

Especially, do not reuse the password associated with your e-mail service.

41 |
42 |
43 | 46 |
47 | ); 48 | } 49 | 50 | export default Registration; 51 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/FormattedText.jsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | import remarkGfm from 'remark-gfm'; 3 | import remarkUnwrapImages from 'remark-unwrap-images'; 4 | import { remarkDefinitionList, defListHastHandlers } from 'remark-definition-list'; 5 | import CroppedImage from './CroppedImage'; 6 | import VideoComment from './VideoComment'; 7 | import FragmentComment from './FragmentComment'; 8 | 9 | function FormattedText({children, setHighlightedText, selectable, setSelectedText}) { 10 | 11 | const handleMouseUp = () => { 12 | if (selectable) { 13 | let text = window.getSelection().toString(); 14 | setSelectedText(text); 15 | setHighlightedText(text); 16 | } 17 | }; 18 | 19 | return (<> 20 | embedVideo(x) || CroppedImage(x), 24 | p: (x) => VideoComment(x) 25 | || FragmentComment({...x, setHighlightedText}) 26 | ||

{x.children}

, 27 | a: ({children, href}) => {children} 28 | }} 29 | remarkRehypeOptions={{ 30 | handlers: defListHastHandlers 31 | }} 32 | > 33 | {children} 34 |
35 | ); 36 | } 37 | 38 | function getId(text) { 39 | const regExp = /^.*(?:youtube\.com\/watch\?v=|youtu\.be\/)([^\s&]{11})/; 40 | const match = text.match(regExp); 41 | return match ? match[1] : null; 42 | } 43 | 44 | function embedVideo({src}) { 45 | const videoId = getId(src); 46 | if (videoId) { 47 | const embedLink = `https://www.youtube.com/embed/${videoId}`; 48 | return ( 49 | 50 | ); 51 | } 52 | return null; 53 | } 54 | 55 | export default FormattedText; 56 | -------------------------------------------------------------------------------- /backend/hyperglosae/src/views/lib/links.js: -------------------------------------------------------------------------------- 1 | exports.parseReference = parseReference = (reference) => { 2 | let [id, fragment] = reference.split('#'); 3 | return {id, fragment}; 4 | } 5 | 6 | // Note: The result set includes also the document itself or its main document 7 | exports.getRelatedDocuments = ({isPartOf, links}) => 8 | new Set( 9 | links.reduce((l, {object, subject}) => [...l, subject, object], []) 10 | .filter(x => !!x) 11 | .concat(isPartOf) 12 | .map(x => parseReference(x).id) 13 | ); 14 | 15 | // Should have the same definition as in `frontend/src/parallelDocuments.js` 16 | const parseText = (text) => { 17 | if (!text) return []; 18 | const PASSAGE = /{([^{]+)} ([^{]*)/g; 19 | let passages = [...text.matchAll(PASSAGE)]; 20 | passages = (passages.length) ? passages : [[null, '0', text]]; 21 | return passages.map(([_, rubric, passage]) => ({ 22 | rubric, 23 | passage, 24 | parsed_rubric: rubric.match(/(?:(\d+)[:., ])?(\d+) ?([a-z]?)/) 25 | .slice(1) 26 | .filter(x => !!x) 27 | .map(x => { 28 | let n = Number(x) ; 29 | return Number.isNaN(n) ? x : n; 30 | }) 31 | })); 32 | } 33 | 34 | exports.emitPassages = ({text, isPartOf, related}) => { 35 | parseText(text).forEach(({rubric, passage, parsed_rubric}) => 36 | related.forEach((x) => { 37 | emit([x, ...parsed_rubric], { text: passage, isPartOf, rubric, _id: null }); 38 | }) 39 | ); 40 | } 41 | 42 | exports.emitIncludedDocuments = ({isPartOf, links}) => { 43 | let includedDocuments = links 44 | .filter(x => x.verb === 'includes') 45 | .map(x => parseReference(x.object)); 46 | includedDocuments.forEach((x, i) => { 47 | emit([isPartOf, i], {inclusion: x.fragment || 'whole', isPartOf, _id: x.id}); 48 | includedDocuments.forEach((y, j) => { 49 | emit([x.id, j], {inclusion: y.fragment || 'whole', isPartOf, _id: y.id}); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/menu-items/DeleteReferenceToDocumentAction.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | import { Modal, Button } from 'react-bootstrap'; 4 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 5 | 6 | function DeleteReferenceToDocumentAction({id, margin, backend, metadata, content, setLastUpdate}) { 7 | const [show, setShow] = useState(false); 8 | const navigate = useNavigate(); 9 | const handleClick = () => { 10 | setShow(false); 11 | metadata.links = metadata.links.filter(link => ![id, ...new Set([...content.filter((row) => row.value.isPartOf === id).map(({id, value}) => value._id || id)])].includes(link.object)); 12 | backend.putDocument({...metadata}) 13 | .then(x => setLastUpdate(x.rev)) 14 | .then(() => navigate('/' + margin + '#' + margin)) 15 | .catch(console.error); 16 | }; 17 | 18 | const disabled = (metadata.links && metadata.links.length > 0 ? false : true); 19 | return ( 20 | <> 21 | setShow(true)} {...{disabled}}> 22 | Delete reference... 23 | 24 | setShow(false)}> 25 | 26 | Are you sure you want to delete the link between this glose and the 27 | focused document? 28 | Both documents will be preserved but in case of a "quotation link", 29 | the quoted contents will be removed from the glose. 30 | This action cannot be undone. 31 | 32 | 33 | 36 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default DeleteReferenceToDocumentAction; 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/CheckboxList.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Form } from 'react-bootstrap'; 3 | 4 | function CheckboxList({ availableItems, selectedItems, setSelectedItems, type}) { 5 | const [isSelectAllChecked, setIsSelectAllChecked] = useState(false); 6 | 7 | const handleSelectAll = () => { 8 | const newSelectAllState = !isSelectAllChecked; 9 | setIsSelectAllChecked(newSelectAllState); 10 | const updatedSelectedItems = newSelectAllState ? [...availableItems] : []; 11 | setSelectedItems(updatedSelectedItems); 12 | }; 13 | 14 | const handleItemChange = (item) => { 15 | let updatedSelectedItems; 16 | 17 | if (selectedItems.includes(item)) { 18 | updatedSelectedItems = selectedItems.filter((i) => i !== item); 19 | } else { 20 | updatedSelectedItems = [...selectedItems, item]; 21 | } 22 | 23 | updatedSelectedItems = availableItems.filter((i) => 24 | updatedSelectedItems.includes(i) 25 | ); 26 | 27 | setSelectedItems(updatedSelectedItems); 28 | setIsSelectAllChecked(updatedSelectedItems.length === availableItems.length); 29 | }; 30 | 31 | return ( 32 |
33 | {availableItems.length > 1 && ( 34 | All} 37 | checked={isSelectAllChecked} 38 | onChange={handleSelectAll} 39 | /> 40 | )} 41 | {availableItems.map((item) => ( 42 | 47 | {type === 'metadata' ? `${item[0]}: ${item[1]}` : item} 48 | 49 | } 50 | checked={selectedItems.includes(item)} 51 | onChange={() => handleItemChange(item)} 52 | /> 53 | ))} 54 |
55 | ); 56 | }; 57 | 58 | export default CheckboxList; 59 | -------------------------------------------------------------------------------- /frontend/scenarios/delete_reference.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Supprimer une référence à un document 4 | 5 | Scénario: dont je suis l'auteur 6 | Soit un document dont je ne suis pas l'auteur affiché comme document principal 7 | Et avec un document reconnaissable dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand je supprime le lien entre le document principal et la référence 10 | Alors il n'y a aucun document principal affiché 11 | Et la glose ouverte est le document reconnaissable 12 | 13 | Scénario: dont je ne suis pas l'auteur 14 | 15 | Soit un document dont je ne suis pas l'auteur affiché comme document principal 16 | Et avec un document reconnaissable dont je ne suis pas l'auteur affiché comme glose 17 | Et une session active avec mon compte 18 | Quand je supprime le lien entre le document principal et la référence 19 | Alors je peux lire "Before editing this document, please request authorization to its editors first" 20 | 21 | Scénario: contenant une citation 22 | 23 | Soit un document reconnaissable dont je suis l'auteur affiché comme glose et dont le type est "Quotation" 24 | Et un document dont je ne suis pas l'auteur affiché comme document principal et contenant : 25 | """ 26 | Les hommes naissent et demeurent libres et égaux en droits. Les distinctions sociales ne peuvent être fondées que sur l'utilité commune. 27 | Le but de toute association politique est la conservation des droits naturels et imprescriptibles de l'Homme. Ces droits sont la liberté, la propriété, la sûreté, et la résistance à l'oppression. 28 | """ 29 | Et une session active avec mon compte 30 | Et je choisis "Quotation" comme type de reférence 31 | Et je réutilise ma glose reconnaissable 32 | Quand je supprime le lien entre le document principal et la référence 33 | Alors il n'y a aucun document principal affiché 34 | Et la glose ouverte est le document reconnaissable 35 | Et les références au document principal contenues dans la glose ne sont plus visibles 36 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import react from 'eslint-plugin-react'; 4 | import reactHooks from 'eslint-plugin-react-hooks'; 5 | import reactRefresh from 'eslint-plugin-react-refresh'; 6 | 7 | export default [ 8 | { ignores: ['build'] }, 9 | { 10 | files: ['src/**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/prop-types': 'off', //for now 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | 'no-unused-vars': [ 37 | 'error', 38 | { destructuredArrayIgnorePattern: '^_' } 39 | ], 40 | 'object-shorthand': 2, 41 | 'brace-style': 2, 42 | 'jsx-quotes': 2, 43 | quotes: [2, 'single'], 44 | 'keyword-spacing': 2, 45 | 'no-else-return': 2, 46 | 'space-infix-ops': 2, 47 | 'comma-spacing': 2, 48 | 'key-spacing': 2, 49 | 50 | semi: [2, 'always', { 51 | omitLastInOneLineBlock: true, 52 | }], 53 | 54 | 'no-multiple-empty-lines': [2, { 55 | max: 1, 56 | }], 57 | 58 | 'no-multi-spaces': 2, 59 | 'no-trailing-spaces': 2, 60 | 'no-spaced-func': 2, 61 | 'space-before-blocks': 2, 62 | 'space-in-parens': [2, 'never'], 63 | 64 | indent: [2, 2, { 65 | SwitchCase: 1, 66 | }], 67 | 68 | 'no-mixed-spaces-and-tabs': 2, 69 | }, 70 | } 71 | ]; 72 | -------------------------------------------------------------------------------- /frontend/scenarios/break_into_passages.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Découper un document en passages numérotés 4 | 5 | Scénario: en fonction du numéro de paragraphe 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose et contenant : 8 | """ 9 | Les hommes naissent et demeurent libres et égaux en droits. Les distinctions sociales ne peuvent être fondées que sur l'utilité commune. 10 | Le but de toute association politique est la conservation des droits naturels et imprescriptibles de l'Homme. Ces droits sont la liberté, la propriété, la sûreté, et la résistance à l'oppression. 11 | """ 12 | Et une session active avec mon compte 13 | Quand je découpe la glose en passages numérotés et que je me focalise sur la glose 14 | Alors la rubrique "1" est associée au passage "Les hommes naissent et demeurent libres" 15 | Et la rubrique "2" est associée au passage "Le but de toute association politique" 16 | 17 | Scénario: en indiquant des rubriques alpha-numériques dans le texte 18 | 19 | Soit un document dont je suis l'auteur affiché comme glose 20 | Et une session active avec mon compte 21 | Quand je remplace le contenu de la glose par ce qui suit et que je me focalise sur la glose : 22 | """ 23 | {447a} CALLICLÈS. C'est ainsi, dit-on, qu'il faut arriver à la guerre et à une bataille. 24 | SOCRATE. Comment ! sommes-bous en retard, et arrivons-nous, comme on dit, après la fête ? 25 | CALLICLÈS. Et même après une fête tout à fait agréable ; car Gorgias vient de nous faire entendre une infinité de belles choses. 26 | SOCRATE. Eh bien, Calliclès, c'est pourtant Chéréphon qui est cause de cela, pour nous avoir obligés de nous arrêter sur la place publique. 27 | 28 | {447b} CHÉRÉPHON. Il n'y a pas de mal, Socrate ; car j'y remédierai bien. Gorgias est mon ami, et par conséquent il se fera entendre à nous à l'instant même, si tu le désires, ou, si tu l'aimes mieux, une autre fois. 29 | """ 30 | Alors la rubrique "447a" est associée au passage "CALLICLÈS. C'est ainsi, dit-on, qu'il faut arriver à la guerre et à une bataille." 31 | Et la rubrique "447b" est associée au passage "CHÉRÉPHON. Il n'y a pas de mal, Socrate ; car j'y remédierai bien. Gorgias" 32 | -------------------------------------------------------------------------------- /frontend/src/menu-items/InviteEditorsAction.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, InputGroup, ListGroup, Modal, Form } from 'react-bootstrap'; 3 | import DiscreeteDropdown from '../components/DiscreeteDropdown'; 4 | 5 | export default function InviteEditorsAction({metadata, backend, setLastUpdate}) { 6 | const [show, setShow] = useState(false); 7 | const [userName, setUserName] = useState(''); 8 | const [grantedEditors, setGrantedEditors] = useState(metadata.editors || []); 9 | 10 | const handleClose = () => setShow(false); 11 | 12 | const handleShow = () => setShow(true); 13 | 14 | let addEditor = () => { 15 | const formattedUserName = userName.trim(); 16 | let editors; 17 | backend.getDocument(metadata._id) 18 | .then(x => { 19 | editors = [...(x.editors || []), formattedUserName]; 20 | return backend.putDocument({...x, editors}); 21 | }) 22 | .then(x => setLastUpdate(x.rev)) 23 | .then(() => { 24 | setGrantedEditors(editors); 25 | setUserName(''); 26 | }) 27 | .catch(console.error); 28 | }; 29 | 30 | return ( 31 | <> 32 | 33 | Invite editors... 34 | 35 | 36 | 37 | 38 | Invite user to edit document 39 | 40 | 41 | Username 42 | 43 | setUserName(event.target.value.toLowerCase())} 47 | onKeyDown={(event) => { 48 | if (event.key === 'Enter') { 49 | addEditor(); 50 | } 51 | }} 52 | /> 53 | 56 | 57 | 58 | 59 |
Editors
60 | 61 | {grantedEditors.map((user) => ( 62 | {user} 63 | ))} 64 | 65 |
66 |
67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /frontend/scenarios/edit_content.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Essayer d'éditer le contenu 4 | 5 | Scénario: de la glose dont on est l'auteur 6 | 7 | Soit un document dont je suis l'auteur affiché comme glose 8 | Et une session active avec mon compte 9 | Quand j'essaie de remplacer le contenu de la glose par : 10 | """ 11 | Nous traduisons ici "shape" par "ombre" et non "forme" car dans un autre 12 | poème du recueil, les "shapes" sont clairement ce qui apparaît en allumant 13 | une lampe dans les ténèbres. 14 | """ 15 | Alors la glose contient "Nous traduisons ici" 16 | 17 | Scénario: de la glose dont on n'est pas l'auteur 18 | 19 | Soit un document dont je ne suis pas l'auteur affiché comme glose 20 | Et une session active avec mon compte 21 | Quand j'essaie de remplacer le contenu de la glose par : 22 | """ 23 | Nous traduisons ici "shape" par "ombre" et non "forme" car dans un autre 24 | poème du recueil, les "shapes" sont clairement ce qui apparaît en allumant 25 | une lampe dans les ténèbres. 26 | """ 27 | Alors je peux lire "Before editing this document, please request authorization to its editors first" 28 | Et la glose est ouverte en mode édition 29 | 30 | Scénario: de la glose sans être connecté 31 | 32 | Soit un document dont je suis l'auteur affiché comme glose 33 | Quand j'essaie de remplacer le contenu de la glose par : 34 | """ 35 | Nous traduisons ici "shape" par "ombre" et non "forme" car dans un autre 36 | poème du recueil, les "shapes" sont clairement ce qui apparaît en allumant 37 | une lampe dans les ténèbres. 38 | """ 39 | Alors je peux lire "Before editing this document, please log in first" 40 | Et la glose est ouverte en mode édition 41 | 42 | Scénario: du document principal lorsqu'il n'a pas de document source 43 | 44 | Soit "Restaurer la vapeur" le document principal 45 | Et qui n'a pas de document source 46 | Et une session active avec mon compte 47 | Quand je souhaite modifier le contenu du document principal 48 | Alors "Restaurer la vapeur" est la glose ouverte en mode édition 49 | 50 | Scénario: du document principal lorsqu'il a un document source 51 | 52 | Soit "Treignes, le 8 septembre 2012 (Christophe Lejeune)" le document principal 53 | Et qui a un document source 54 | Et une session active avec mon compte 55 | Quand je souhaite modifier le contenu du document principal 56 | Alors "Treignes, le 8 septembre 2012 (Christophe Lejeune)" est la glose ouverte en mode édition -------------------------------------------------------------------------------- /frontend/src/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/Menu.css'; 2 | 3 | import Navbar from 'react-bootstrap/Navbar'; 4 | import Container from 'react-bootstrap/Container'; 5 | import Col from 'react-bootstrap/Col'; 6 | import Row from 'react-bootstrap/Row'; 7 | import Form from 'react-bootstrap/Form'; 8 | import Dropdown from 'react-bootstrap/Dropdown'; 9 | import { Link } from 'react-router'; 10 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 11 | import SignOutAction from '../menu-items/SignOutAction'; 12 | 13 | function Menu({backend, user, setUser}) { 14 | return ( 15 | 16 | 17 | 18 | Consult my bookshelf} placement="bottom"> 19 | 20 | Index 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | function Authentication({backend, user, setUser}) { 35 | 36 | let handleSubmit = (e) => { 37 | e.preventDefault(); 38 | let credentials = Object.fromEntries(new FormData(e.target).entries()); 39 | backend.postSession(credentials) 40 | .then(setUser); 41 | }; 42 | 43 | if (user) return ( 44 | 45 | 46 | {user} 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | return ( 54 |
55 | 56 | 57 | 60 | 61 | 62 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | Register... 74 | 75 | 76 | 77 |
78 | ); 79 | } 80 | 81 | export default Menu; 82 | -------------------------------------------------------------------------------- /samples/hyperglosae/ethnography04.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "6b56ee657c870dfacd34e9ae4e0643dd", 3 | "links": [ 4 | { 5 | "verb": "refersTo", 6 | "object": "6b56ee657c870dfacd34e9ae4e0576ce" 7 | } 8 | ], 9 | "editors": [ 10 | "christophe" 11 | ], 12 | "dc_license": "https://creativecommons.org/licenses/by/4.0/", 13 | "text": "{1} Lors d'une de mes premières venues à l'atelier, le nettoyage de la carrosserie d'une locomotive à vapeur m'est confié. Pendant ce temps, Luc, un autre bénévole la met en chauffe. Il s'agit d'allumer le feu dans le foyer de manière à progressivement réchauffer l'eau présente dans la chaudière, qui, se transformant en vapeur, fournit la force motrice d'une telle locomotive. \n \n{2} Assez rapidement, plusieurs personnes se présentent à moi. Ayant identifié que je suis nouveau, elles me souhaitent la bienvenue. Un premier membre me trouve un seau plus grand que celui avec lequel j'ai commencé le nettoyage. Un autre m'indique que j'aurais intérêt à commencer par les parties hautes de la locomotive, plutôt que des côtés, car celles-ci deviendront vite très chaudes (je risque de me bruler si j'attends pour les laver). \n \n{3} La chauffe dure à trois à quatre heures. Une fois que la locomotive est prête, des essais ont lieu aux abords de l'atelier. Pour réaliser ces essais, Leo se joint à Luc. Ces essais consistent à faire quelques manœuvres au sein du faisceau (rail et aiguillage) aux alentours de l'atelier. Très vite, Leo descend de la locomotive, plonge son bras sous la carrosserie, et affichant une moue crispée, affirme que les boîtes d'essieu chauffent trop. \n \n{4} Il est midi. Nous interrompons tous les trois les essais et rejoignons les autres bénévoles pour le repas de midi. La météo étant clémente, nous sortons quelques chaises pour manger en plein air. Tout en mangeant, Luc et Leo exposent les résultats des essais. Les différentes personnes présentes discutent des solutions possibles. J'apprends alors que le problème est connu et récurrent sur cette ancienne locomotive de charbonnage. Afin de faire diminuer ces *hot boxes*, les bénévoles ont déjà testé différents types d'huiles de plus en plus épaisses. C'est un des administrateurs de l'association, travaillant dans l'industrie, qui a identifié l'huile épaisse utilisée lors des essais. Un autre convive m'explique que cette huile est tellement épaisse qu'elle donne une sensation de velour lorsqu'on la fait glisser entre les doigts.", 14 | "dc_issued": "2012", 15 | "dc_language": "french", 16 | "dc_title": "Treignes, le 8 septembre 2012", 17 | "dc_creator": "Christophe Lejeune" 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/routes/Bookshelf.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/Bookshelf.css'; 2 | import { useState, useEffect } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import ButtonGroup from 'react-bootstrap/ButtonGroup'; 5 | import ToggleButton from 'react-bootstrap/ToggleButton'; 6 | import FutureDocument from '../components/FutureDocument.jsx'; 7 | import DocumentsCards from '../components/DocumentsCards.jsx'; 8 | import Graph from '../components/Graph.jsx'; 9 | import Col from 'react-bootstrap/Col'; 10 | import Row from 'react-bootstrap/Row'; 11 | 12 | function Bookshelf({ backend, user }) { 13 | const [documents, setDocuments] = useState([]); 14 | const [lastUpdate, setLastUpdate] = useState(); 15 | const [displayMode, setDisplayMode] = useState(localStorage.getItem('displayMode') || 'graph'); 16 | 17 | const displayModesList = ['graph', 'list']; 18 | 19 | useEffect(() => { 20 | backend.getAllDocuments(user) 21 | .then(setDocuments); 22 | }, [lastUpdate, user, backend]); 23 | const docs = [ 24 | ...new Map( 25 | documents?.filter(x => !!x) 26 | .map(({_id, dc_title, links}) => [_id, [_id, dc_title, links]]) 27 | ).values() 28 | ]; 29 | const displayedDocs = docs?.flatMap(d => d[0]); 30 | 31 | function DisplayDocuments() { 32 | switch (displayMode) { 33 | case 'graph': 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | case 'list': 45 | return ; 48 | } 49 | } 50 | 51 | return ( 52 | 53 |

My documents

54 | 55 | {displayModesList.map((display, idx) => ( 56 | { 65 | setDisplayMode(e.currentTarget.value); 66 | localStorage.setItem('displayMode', e.currentTarget.value); 67 | }} 68 | > 69 | as a {display} 70 | 71 | ))} 72 | 73 | 74 |
75 | ); 76 | } 77 | 78 | export default Bookshelf; 79 | 80 | -------------------------------------------------------------------------------- /frontend/scenarios/open_document.feature: -------------------------------------------------------------------------------- 1 | #language: fr 2 | 3 | Fonctionnalité: Ouvrir à côté d'une source 4 | 5 | Scénario: une traduction passage par passage 6 | 7 | Soit "Les fées (Charles Perrault)" le document principal 8 | Et "A tündérek (Charles Perrault)" une des gloses 9 | Et "Víly (Charles Perrault)" une des gloses 10 | Et je peux lire "Il était une fois une veuve qui avait deux filles" 11 | Mais je ne peux pas lire "Volt egyszer egy özvegyasszony" 12 | Et je ne peux pas lire "Bola raz jedna vdova, ktorá mala dve dcéry" 13 | Quand j'ouvre "Víly (Charles Perrault)" à côté 14 | Alors je peux lire "Bola raz jedna vdova, ktorá mala dve dcéry" 15 | Et je peux lire "Il était une fois une veuve qui avait deux filles" 16 | Mais je ne peux pas lire "Volt egyszer egy özvegyasszony" 17 | 18 | Scénario: un commentaire fragment par fragment 19 | 20 | Soit "Entretien avec un responsable d'opération" le document principal 21 | Et "Étiquetage de l'entretien" une des gloses 22 | Et je peux lire "le matin tu arrives sur le chantier, tu vas fouiller selon le programme que t'as établi" 23 | Quand j'ouvre "Étiquetage de l'entretien" à côté 24 | Alors je peux lire "le matin tu arrives sur le chantier, tu vas fouiller selon le programme que t'as établi" 25 | Et je peux lire "fouiller selon le programmeAction" 26 | 27 | Scénario: un commentaire global 28 | 29 | Soit "Étiquetage de l'entretien" le document principal 30 | Et "Analyse de l'entretien" une des gloses 31 | Et je peux lire "photos" 32 | Mais je ne peux pas lire "photographies" 33 | Quand j'ouvre "Analyse de l'entretien" à côté 34 | Alors je peux lire "photos" 35 | Et je peux lire "l'archéologie transforme les vestiges en documents ([photographies], plans, [coupes], etc.)." 36 | 37 | Scénario: une comparaison avec des documents apparentés 38 | 39 | Soit "Photographie : vitrail, baie 113, Église Saint-Nizier, Troyes" le document principal 40 | Et "Soleil noir et étoiles qui tombent – Comparaison de vitraux" une des gloses 41 | Et "Lavage des tuniques – Comparaison de vitraux" une des gloses 42 | Et je vois l'image "SNZ 113" dans le document principal 43 | Mais je ne vois pas l'image "GRV 005" 44 | Et je ne vois pas l'image "SMV 006 Soleil" 45 | Et je ne vois pas l'image "SMV 006 Lavage" 46 | Quand j'ouvre "Soleil noir et étoiles qui tombent – Comparaison de vitraux" à côté 47 | Alors je vois l'image "GRV 005" dans la glose 48 | Et je vois l'image "SMV 006 Soleil" dans la glose 49 | Mais je ne vois pas l'image "SMV 006 Lavage" 50 | Et je vois l'image "SNZ 113" dans la glose 51 | Et l'image intégrée dans la page a pour légende "Photographie : vitrail, baie 113, Église Saint-Nizier, Troyes" 52 | Et l'image intégrée dans la page a pour légende "Photographie : vitrail, baie 6, Église Saint-Martin-ès-Vignes, Troyes" 53 | Et l'image intégrée dans la page a pour légende "Photographie : vitrail, baie 5, Église Saint-Martin, Grandville, Aube" 54 | 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hyperglosae 2 | 3 | ## Requirements 4 | 5 | - A UNIX-like terminal (preferably WSL if you are on Windows), 6 | - Git, 7 | - Node.js (preferably through nvm), 8 | - Docker (preferably through Docker Desktop). 9 | 10 | ## Installing software 11 | 12 | Get the code: 13 | 14 | ```shell 15 | git clone https://github.com/Hypertopic/HyperGlosae.git 16 | ``` 17 | 18 | Install the backend: 19 | 20 | ```shell 21 | docker compose --file docker-compose.dev.yml pull 22 | ``` 23 | 24 | Install the frontend: 25 | 26 | ``` 27 | cd frontend 28 | npm install 29 | ``` 30 | 31 | ## Launching software 32 | 33 | From the root folder of Hyperglosae, launch the backend: 34 | 35 | ```shell 36 | export COUCHDB_USER="TO_BE_CHANGED" 37 | export COUCHDB_PASSWORD="TO_BE_CHANGED" 38 | docker compose --file docker-compose.dev.yml up --detach 39 | ``` 40 | 41 | Open to view its Web console. 42 | 43 | From the `frontend` folder of Hyperglosae, compile and launch the frontend: 44 | 45 | ``` 46 | npm start 47 | ``` 48 | Don't close the terminal or interrupt the command unless you want to "kill" the service. 49 | 50 | 51 | Open to browse sample data in the application. 52 | To test edit features, log in as user `alice` with `whiterabbit` as the password. 53 | 54 | ## Running tests 55 | 56 | From the `frontend` folder of Hyperglosae, type the following command: 57 | 58 | ```shell 59 | npm run test2 60 | ``` 61 | 62 | Select `E2E testting`, `Electron`, and then the tests you want to run. 63 | 64 | ## Developping 65 | 66 | ### Frontend developping 67 | 68 | The frontend is coded in JavaScript with the React framework (see [documentation](https://reactjs.org/docs/getting-started.html)). 69 | 70 | Everytime you update code in `frontend/src`, the frontend page is reloaded. 71 | You may also see any lint errors in the console. 72 | 73 | ### Backend developping 74 | 75 | The backend is coded in JavaScript as CouchDB views (see [documentation](https://docs.couchdb.org/en/stable/ddocs/views/)). Documents stored in CouchDB can be created, updated and deleted (esp. by the frontend) using CouchDB REST API (see [documentation](https://docs.couchdb.org/en/stable/api/document/)). 76 | 77 | Everytime you update code or settings in `backend`, please push them to the backend with: 78 | 79 | ```shell 80 | docker compose --file docker-compose.dev.yml run updated_code 81 | ``` 82 | 83 | And then refresh the frontend (or backend) page. 84 | 85 | ### Samples writing 86 | 87 | Everytime you update sample data in `samples`, please push them to the backend with: 88 | 89 | ```shell 90 | docker compose --file docker-compose.dev.yml run updated_samples 91 | ``` 92 | 93 | ### Cleaning the data 94 | 95 | If (and only if) you want to remove ANY DATA added by hand or through automated tests, launch the following command: 96 | 97 | ```shell 98 | docker-compose --file docker-compose.dev.yml down 99 | docker compose --file docker-compose.dev.yml up --detach 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /frontend/src/components/Metadata.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/Metadata.css'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { parse, stringify } from 'yaml'; 5 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 6 | 7 | function Metadata({metadata = {}, editable, backend, setLastUpdate}) { 8 | const [beingEdited, setBeingEdited] = useState(false); 9 | const [editedDocument, setEditedDocument] = useState(metadata); 10 | 11 | useEffect(() => { 12 | setEditedDocument(metadata); 13 | }, [metadata]); 14 | 15 | let handleClick = () => { 16 | setBeingEdited(true); 17 | backend.getDocument(metadata._id) 18 | .then((x) => { 19 | setEditedDocument(x); 20 | }); 21 | }; 22 | 23 | let handleBlur = () => { 24 | setBeingEdited(false); 25 | let updatedDocument = { 26 | ...Object.fromEntries( 27 | Object.entries(editedDocument).filter(([key, _]) => !key.startsWith('dc_')) 28 | ), 29 | ...parse(event.target.value) 30 | }; 31 | setEditedDocument(updatedDocument); 32 | backend.putDocument(updatedDocument) 33 | .then(x => setLastUpdate(x.rev)) 34 | .catch(console.error); 35 | }; 36 | 37 | let editedMetadata = Object.fromEntries( 38 | Object.entries(editedDocument) 39 | .filter(([key, _]) => key.startsWith('dc_')) 40 | ); 41 | 42 | let format = (actors, prefix = '', suffix = '') => 43 | actors && (prefix + [actors].flat().join(' & ') + suffix); 44 | 45 | let capitalize = (name) => 46 | name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); 47 | 48 | let formatTranslation = (translators = '', language = '') => 49 | (translators) && 50 | 'Translated ' 51 | + (language && `in ${capitalize(language)} `) 52 | + format(translators, 'by ', ', '); 53 | 54 | let getCaption = ({dc_title, dc_spatial}) => dc_title + (dc_spatial ? `, ${dc_spatial}` : ''); 55 | 56 | if (!beingEdited) { 57 | let {dc_title, dc_spatial, dc_creator, dc_translator, dc_isPartOf, dc_issued, dc_language} = editedMetadata; 58 | let formattedMetadata = ( 59 | <> 60 | 61 | {getCaption({dc_title, dc_spatial})} {format(dc_creator, '(', ')')}, 62 | 63 | 64 | {formatTranslation(dc_translator, dc_language)} 65 | {dc_isPartOf ? {dc_isPartOf}, : ''} 66 | {dc_issued ? `${new Date(dc_issued.toString()).getFullYear()}` : ''} 67 | 68 | 69 | ); 70 | if (editable) return ( 71 | Edit metadata...} > 72 | 73 | {formattedMetadata} 74 | 75 | 76 | ); 77 | return ( 78 | 79 | {formattedMetadata} 80 | 81 | ); 82 | } 83 | return ( 84 |
85 |