├── linkedrw ├── __main__.py ├── scraper │ ├── __init__.py │ ├── scrape.py │ ├── accomplishment.py │ ├── personal.py │ └── background.py ├── exceptions.py ├── linkedr │ ├── __init__.py │ ├── skill.py │ ├── publication.py │ ├── section.py │ └── resume.py ├── linkedw │ ├── __init__.py │ └── website.py ├── __init__.py ├── templates │ ├── awesome_cv_files │ │ └── fonts │ │ │ ├── FontAwesome.ttf │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-Thin.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-LightItalic.ttf │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ └── Roboto-ThinItalic.ttf │ ├── dev_portfolio_files │ │ ├── images │ │ │ └── lead-bg.jpg │ │ ├── libs │ │ │ └── font-awesome │ │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ │ │ ├── less │ │ │ │ ├── screen-reader.less │ │ │ │ ├── fixed-width.less │ │ │ │ ├── larger.less │ │ │ │ ├── list.less │ │ │ │ ├── core.less │ │ │ │ ├── stacked.less │ │ │ │ ├── font-awesome.less │ │ │ │ ├── bordered-pulled.less │ │ │ │ ├── rotated-flipped.less │ │ │ │ ├── path.less │ │ │ │ ├── animated.less │ │ │ │ └── mixins.less │ │ │ │ └── scss │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── font-awesome.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _path.scss │ │ │ │ ├── _animated.scss │ │ │ │ └── _mixins.scss │ │ ├── package.json │ │ ├── gulpfile.js │ │ ├── js │ │ │ ├── scripts.min.js │ │ │ └── scripts.js │ │ ├── css │ │ │ ├── styles.css │ │ │ └── bootstrap.min.css │ │ └── scss │ │ │ └── styles.scss │ ├── resume_template.tex │ └── index_template.html ├── utils │ ├── __init__.py │ ├── get_prog_languages.py │ ├── helper.py │ └── prog_languages.txt ├── constants.py └── main.py ├── setup.cfg ├── .gitattributes ├── MANIFEST.in ├── .coveragerc ├── requirements.txt ├── tests ├── resume │ ├── publications.tex │ ├── honors.tex │ ├── skills.tex │ ├── volunteering.tex │ ├── projects.tex │ ├── education.tex │ ├── experience.tex │ ├── references.bib │ └── resume.tex ├── test_website.py ├── profile.json ├── test_resume.py └── website │ ├── empty │ └── index.html │ ├── no_date │ └── index.html │ └── full │ └── index.html ├── .github ├── ISSUE_TEMPLATE.md └── stale.yml ├── LICENSE ├── .travis.yml ├── setup.py ├── example.json ├── README.md └── .gitignore /linkedrw/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /linkedrw/scraper/__init__.py: -------------------------------------------------------------------------------- 1 | from .scrape import scrape 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest --addopts=--cov=./ 3 | 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.cls linguist-vendored 2 | *.sty linguist-vendored -------------------------------------------------------------------------------- /linkedrw/exceptions.py: -------------------------------------------------------------------------------- 1 | class LoginError(BaseException): 2 | pass 3 | -------------------------------------------------------------------------------- /linkedrw/linkedr/__init__.py: -------------------------------------------------------------------------------- 1 | from .resume import make_resume_files 2 | -------------------------------------------------------------------------------- /linkedrw/linkedw/__init__.py: -------------------------------------------------------------------------------- 1 | from .website import make_website_files 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE requirements.txt linkedrw/utils/* 2 | recursive-include linkedrw/templates/ * -------------------------------------------------------------------------------- /linkedrw/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | __version__ = "1.2.1" 4 | VERSION = __version__ 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | linkedrw/main.py 4 | linkedrw/scraper/* 5 | linkedrw/utils/* 6 | setup.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | arrow>=0.13.1 2 | beautifulsoup4>=4.7.1 3 | habanero>=0.6.2 4 | logbook>=1.4.3 5 | requests>=2.21.0 6 | selenium>=3.141.0 7 | -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/images/lead-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/images/lead-bg.jpg -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /tests/resume/publications.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Publications} 2 | 3 | \begin{refsection} 4 | \nocite{Bradford_1976} 5 | \printbibliography[heading=none] 6 | \end{refsection} -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /linkedrw/templates/awesome_cv_files/fonts/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/awesome_cv_files/fonts/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /tests/resume/honors.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Honors \& Awards} 2 | 3 | \begin{cvhonors} 4 | \cvhonor 5 | {Award} % title 6 | {Issuer} % issuer 7 | {Location} % location 8 | {2019} % date 9 | \end{cvhonors} -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeshuaro/LinkedRW/HEAD/linkedrw/templates/dev_portfolio_files/libs/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /tests/resume/skills.tex: -------------------------------------------------------------------------------- 1 | \cvsection{skills} 2 | 3 | \begin{cvskills} 4 | \cvskill 5 | {Programming} 6 | {Python, Java} 7 | \cvskill 8 | {Technologies} 9 | {Docker} 10 | \cvskill 11 | {Languages} 12 | {English} 13 | \end{cvskills} -------------------------------------------------------------------------------- /tests/resume/volunteering.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Volunteering} 2 | 3 | \begin{cventries} 4 | \cventry 5 | {Title} % title 6 | {Volunteering Centre} % name 7 | {Location} % location 8 | {Jan 2018 {-} Dec 2018} % dates 9 | {} % description 10 | 11 | \end{cventries} -------------------------------------------------------------------------------- /linkedrw/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from linkedrw.utils.helper import ( 2 | make_dir, 3 | get_accomplishment_link, 4 | get_description, 5 | get_optional_text, 6 | get_optional_text_replace, 7 | get_span_text, 8 | escape_latex, 9 | copy_files, 10 | scroll_to_elem, 11 | ) 12 | -------------------------------------------------------------------------------- /tests/resume/projects.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Projects} 2 | 3 | \begin{cventries} 4 | \cventry 5 | {} 6 | {Project} % name 7 | {} 8 | {Jan 2018 {-} Present} % dates 9 | { 10 | \begin{cvitems} 11 | \item{Description} 12 | \end{cvitems} 13 | } 14 | 15 | \end{cventries} -------------------------------------------------------------------------------- /tests/resume/education.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Education} 2 | 3 | \begin{cventries} 4 | \cventry 5 | {Degree} % degree 6 | {School} % name 7 | {Location} % location 8 | {2018 {-} 2019} % dates 9 | { 10 | \begin{cvitems} 11 | \item{Description} 12 | \end{cvitems} 13 | } 14 | 15 | \end{cventries} -------------------------------------------------------------------------------- /tests/resume/experience.tex: -------------------------------------------------------------------------------- 1 | \cvsection{Experience} 2 | 3 | \begin{cventries} 4 | \cventry 5 | {Title} % title 6 | {Company} % name 7 | {Location} % location 8 | {Jan 2019 {-} Present} % dates 9 | { 10 | \begin{cvitems} 11 | \item{Description} 12 | \end{cvitems} 13 | } 14 | 15 | \end{cventries} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue Template 3 | about: Template for issue report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What happened 11 | 12 | ### Did you keep your browser window on top when `linkedrw` is running 13 | 14 | ### Did you try increasing the timeout by using `-t/--timeout` option 15 | 16 | ### Which web driver did you use 17 | 18 | ### Logs 19 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /tests/resume/references.bib: -------------------------------------------------------------------------------- 1 | @article{Bradford_1976, 2 | doi = {10.1016/0003-2697(76)90527-3}, 3 | url = {https://doi.org/10.1016%2F0003-2697%2876%2990527-3}, 4 | year = 1976, 5 | month = {may}, 6 | publisher = {Elsevier {BV}}, 7 | volume = {72}, 8 | number = {1-2}, 9 | pages = {248--254}, 10 | author = {Marion M. Bradford}, 11 | title = {A rapid and sensitive method for the quantitation of microgram quantities of protein utilizing the principle of protein-dye binding}, 12 | journal = {Analytical Biochemistry} 13 | } -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devportfolio-template", 3 | "version": "1.1.3", 4 | "description": "", 5 | "main": "js/scripts.js", 6 | "scripts": { 7 | "watch": "gulp watch" 8 | }, 9 | "keywords": [ 10 | "portfolio", 11 | "personal", 12 | "website", 13 | "tech", 14 | "coding", 15 | "dev" 16 | ], 17 | "author": "Ryan Fitzgerald", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "gulp": "^4.0.0", 21 | "gulp-autoprefixer": "^6.0.0", 22 | "gulp-plumber": "^1.2.0", 23 | "gulp-rename": "^1.4.0", 24 | "gulp-sass": "^4.0.2", 25 | "gulp-uglify": "^3.0.1", 26 | "gulp-wait": "0.0.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /linkedrw/utils/get_prog_languages.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | from bs4 import BeautifulSoup 5 | 6 | 7 | def main(): 8 | url = "https://en.wikipedia.org/wiki/List_of_programming_languages" 9 | r = requests.get(url) 10 | languages = [] 11 | 12 | if r.status_code == 200: 13 | soup = BeautifulSoup(r.text, "html.parser") 14 | divs = soup.find_all("div", {"class": "div-col columns column-width"}) 15 | 16 | for div in divs: 17 | for li in div.find_all("li"): 18 | language = li.text.strip().lower() 19 | language = re.sub("\s+\(.*", "", language) 20 | language = re.sub("\s+–.*", "", language) 21 | languages.append(language) 22 | 23 | with open("prog_languages.txt", "w") as f: 24 | f.write("\n".join(languages)) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zeshuaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var plumber = require('gulp-plumber'); 3 | var uglify = require('gulp-uglify'); 4 | var sass = require('gulp-sass'); 5 | var wait = require('gulp-wait'); 6 | var rename = require('gulp-rename'); 7 | var autoprefixer = require('gulp-autoprefixer'); 8 | 9 | gulp.task('scripts', function() { 10 | return gulp.src('js/scripts.js') 11 | .pipe(plumber(plumber({ 12 | errorHandler: function (err) { 13 | console.log(err); 14 | this.emit('end'); 15 | } 16 | }))) 17 | .pipe(uglify({ 18 | output: { 19 | comments: '/^!/' 20 | } 21 | })) 22 | .pipe(rename({extname: '.min.js'})) 23 | .pipe(gulp.dest('js')); 24 | }); 25 | 26 | gulp.task('styles', function () { 27 | return gulp.src('./scss/styles.scss') 28 | .pipe(wait(250)) 29 | .pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError)) 30 | .pipe(gulp.dest('./css')); 31 | }); 32 | 33 | gulp.task('watch', function() { 34 | gulp.watch('js/scripts.js', gulp.series('scripts')); 35 | gulp.watch('scss/styles.scss', gulp.series('styles')); 36 | }); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial # required for Python >= 3.7 2 | language: python 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | 7 | branches: 8 | only: 9 | - master 10 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ 11 | 12 | cache: 13 | directories: 14 | - $HOME/.cache/pip 15 | - $HOME/.pre-commit 16 | before_cache: 17 | - rm -f $HOME/.cache/pip/log/debug.log 18 | - rm -f $HOME/.pre-commit/pre-commit.log 19 | 20 | # command to install dependencies 21 | install: pip install -r requirements.txt codecov 22 | 23 | # command to run tests 24 | script: python setup.py test 25 | 26 | jobs: 27 | include: 28 | - stage: deploy 29 | if: tag IS present 30 | python: "3.6" 31 | install: skip 32 | script: skip 33 | deploy: 34 | provider: pypi 35 | user: zeshuaro 36 | password: 37 | secure: JxSsLwMmZO2369d/YxX9vadFeuVej4JOpAJz6DXJkTaQhvyGS8lkD4KeBpBeXTC6Y2TZ3lkW68z/6bNBSZ9Vjh+9vKUL+Gkmc0m5RZIyZBw7gu0re+0XxBocJp5eWK24g2Sj36hyNB3zs7BakPSYMUO36Zh9+tE0AFrl7j7oaXccEtYwb3h5srpe4ANc0/PdriE+YteJDE6gMe6obKuVMrdZgGoErgoRALWfapcNNVGYRNbKEYpoD37GGqgteULKTGb7gUv+pyY4N6OsEW3Xu+gptlQysEB09DTyholxIUARv2AMbQUn0juTBneTJC+YKSMyUQTA2muy9WEkQdf2ZxsUi3W3BynmKgt6u6kepB5sNLvuVCe0i2TTlHSea1G519tUJs2WwMdXFDSmFFFyO4QV+70cZs+lQaYglDo6fwE7GWwB1Fb7tkL2a6ftp/eAoNeWJX7nvtGZJMRR8Sv8ic/Ugc4wHF4xGfIsjZSxjg1JmbevIyk2chdxdB0Hpi22of1b4B3G8QX4jR3p2URwUM30WcGujT8or2XBonE9saACl7t84KtVkDErz0e1zeqqNj08MEniOVrviGCvHU+G811EbezSmzuwH81R92ka+n8x2cV1SLfBy0L8tlrgT30YA/gH4HjzdTBncTD1hfuLhKMfrYcelv/l99eFZPJ6FPE= 38 | on: 39 | branch: master 40 | tags: true 41 | 42 | after_success: 43 | - coverage combine 44 | - codecov -F Travis -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/js/scripts.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Title: Dev Portfolio Template 3 | Version: 1.2.1 4 | Last Change: 08/27/2017 5 | Author: Ryan Fitzgerald 6 | Repo: https://github.com/RyanFitzgerald/devportfolio-template 7 | Issues: https://github.com/RyanFitzgerald/devportfolio-template/issues 8 | 9 | Description: This file contains all the scripts associated with the single-page 10 | portfolio website. 11 | */ 12 | !function(n){n("html").removeClass("no-js"),n("header a").click(function(e){if(!n(this).hasClass("no-scroll")){e.preventDefault();var t=n(this).attr("href"),i=n(t).offset().top;n("html, body").animate({scrollTop:i+"px"},Math.abs(window.pageYOffset-n(t).offset().top)/1),n("header").hasClass("active")&&n("header, body").removeClass("active")}}),n("#to-top").click(function(){n("html, body").animate({scrollTop:0},500)}),n("#lead-down span").click(function(){var e=n("#lead").next().offset().top;n("html, body").animate({scrollTop:e+"px"},500)}),n("#experience-timeline").each(function(){$this=n(this),$userContent=$this.children("div"),$userContent.each(function(){n(this).addClass("vtimeline-content").wrap('
')}),$this.find(".vtimeline-point").each(function(){n(this).prepend('
')}),$this.find(".vtimeline-content").each(function(){var e=n(this).data("date");e&&n(this).parent().prepend(''+e+"")})}),n("#mobile-menu-open").click(function(){n("header, body").addClass("active")}),n("#mobile-menu-close").click(function(){n("header, body").removeClass("active")}),n("#view-more-projects").click(function(e){e.preventDefault(),n(this).fadeOut(300,function(){n("#more-projects").fadeIn(300)})})}(jQuery); -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/libs/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import re 4 | 5 | from setuptools import setup, find_packages 6 | 7 | 8 | def requirements(): 9 | """ 10 | Build the requirements list for this project 11 | Returns: 12 | A list of requirements 13 | """ 14 | requirements_list = [] 15 | with open("requirements.txt") as f: 16 | for line in f: 17 | requirements_list.append(line.strip()) 18 | 19 | return requirements_list 20 | 21 | 22 | def fpath(name): 23 | return os.path.join(os.path.dirname(__file__), name) 24 | 25 | 26 | def read(fname): 27 | return codecs.open(fpath(fname), encoding="utf-8").read() 28 | 29 | 30 | def grep(attrname): 31 | pattern = r'{}\W*=\W*"([^"]+)"'.format(attrname) 32 | strval, = re.findall(pattern, read(fpath("linkedrw/__init__.py"))) 33 | return strval 34 | 35 | 36 | setup( 37 | name="linkedrw", 38 | version=grep("__version__"), 39 | author="Joshua Tang", 40 | author_email="zeshuaro@gmail.com", 41 | license="MIT License", 42 | url="https://github.com/zeshuaro/LinkedRW", 43 | keywords="python scraper cv resume portfolio profile website", 44 | description="A simple CLI for you to create your resume and personal website based on your LinkedIn profile", 45 | long_description=open("README.md").read(), 46 | long_description_content_type="text/markdown", 47 | packages=find_packages(), 48 | python_requires=">=3.6", 49 | install_requires=requirements(), 50 | setup_requires=["pytest-runner"], 51 | tests_require=["pytest", "pytest-cov"], 52 | include_package_data=True, 53 | entry_points={"console_scripts": ["linkedrw=linkedrw:main"]}, 54 | classifiers=[ 55 | "Development Status :: 5 - Production/Stable", 56 | "Intended Audience :: Developers", 57 | "License :: OSI Approved :: MIT License", 58 | "Operating System :: OS Independent", 59 | "Programming Language :: Python", 60 | "Programming Language :: Python :: 3.6", 61 | "Programming Language :: Python :: 3.7", 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /tests/test_website.py: -------------------------------------------------------------------------------- 1 | import filecmp 2 | import json 3 | import os 4 | import pkg_resources 5 | import tempfile 6 | 7 | from linkedrw.constants import ( 8 | EDUCATION, 9 | NAME, 10 | ENTRIES, 11 | DEGREE, 12 | LOCATION, 13 | DATES, 14 | DESCRIPTION, 15 | ) 16 | from linkedrw.linkedw.website import make_website_files 17 | 18 | WEBSITE_DIR = "website" 19 | PROFILE_FILE = "profile.json" 20 | 21 | 22 | def test_make_resume_files_full(): 23 | with open(pkg_resources.resource_filename(__name__, PROFILE_FILE)) as f: 24 | profile = json.load(f) 25 | 26 | check_outputs(profile, "full") 27 | 28 | 29 | def test_make_resume_files_empty(): 30 | check_outputs({}, "empty") 31 | 32 | 33 | def test_make_resume_files_no_date(): 34 | profile = { 35 | EDUCATION: [ 36 | { 37 | NAME: "Name", 38 | ENTRIES: [ 39 | { 40 | DEGREE: "Degree 1", 41 | DATES: "", 42 | LOCATION: "Location 1", 43 | DESCRIPTION: "Description 1", 44 | }, 45 | { 46 | DEGREE: "Degree 2", 47 | DATES: "2018 - 2019", 48 | LOCATION: "Location 2", 49 | DESCRIPTION: "Description 2", 50 | }, 51 | ], 52 | } 53 | ] 54 | } 55 | check_outputs(profile, "no_date") 56 | 57 | 58 | def check_outputs(profile, files_dir): 59 | with tempfile.TemporaryDirectory() as dirname: 60 | make_website_files(profile, dirname) 61 | for filename in os.listdir( 62 | pkg_resources.resource_filename(__name__, f"{WEBSITE_DIR}/{files_dir}") 63 | ): 64 | assert ( 65 | filecmp.cmp( 66 | os.path.join(dirname, WEBSITE_DIR, filename), 67 | pkg_resources.resource_filename( 68 | __name__, f"{WEBSITE_DIR}/{files_dir}/{filename}" 69 | ), 70 | shallow=False, 71 | ) 72 | is True 73 | ) 74 | -------------------------------------------------------------------------------- /linkedrw/linkedr/skill.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkg_resources 3 | 4 | from linkedrw.constants import SKILLS, LATEX_INDENT, PACKAGE_NAME 5 | from linkedrw.utils import escape_latex 6 | 7 | 8 | def make_skill_section(skills, languages, output_dir): 9 | """ 10 | Create skill latex files 11 | Args: 12 | skills: the list of skills 13 | languages: the list of languages 14 | output_dir: the output directory 15 | 16 | Returns: 17 | None 18 | """ 19 | if skills or languages: 20 | lines = [f"\\cvsection{{{SKILLS}}}\n", "\\begin{cvskills}"] 21 | 22 | if skills: 23 | # Get and store a set of programming languages 24 | prog_languages = set() 25 | with open( 26 | pkg_resources.resource_filename( 27 | PACKAGE_NAME, "utils/prog_languages.txt" 28 | ) 29 | ) as f: 30 | for line in f: 31 | prog_languages.add(line.strip()) 32 | 33 | prog = [] 34 | tech = [] 35 | 36 | # Check if skill is a programming language 37 | for skill in skills: 38 | if skill.lower() in prog_languages: 39 | prog.append(skill) 40 | else: 41 | tech.append(skill) 42 | 43 | lines += make_skill_subsection(prog, "Programming") 44 | lines += make_skill_subsection(tech, "Technologies") 45 | 46 | lines += make_skill_subsection(languages, "Languages") 47 | lines.append("\\end{cvskills}") 48 | 49 | with open(os.path.join(output_dir, f"{SKILLS}.tex"), "w") as f: 50 | f.write("\n".join(lines)) 51 | 52 | 53 | def make_skill_subsection(skills, skills_type): 54 | """ 55 | Create the lines for the skill subsection 56 | Args: 57 | skills: the list of skills 58 | skills_type: the skill type 59 | 60 | Returns: 61 | A list of lines for the skill subsection 62 | """ 63 | lines = [] 64 | if skills: 65 | lines.append(f"{LATEX_INDENT}\\cvskill") 66 | lines.append(f"{LATEX_INDENT * 2}{{{skills_type}}}") 67 | lines.append(f'{LATEX_INDENT * 2}{{{escape_latex(", ".join(skills))}}}') 68 | 69 | return lines 70 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Name", 3 | "position": "Current position", 4 | "contact": { 5 | "address": "", 6 | "mobile": "", 7 | "email": "", 8 | "homepage": "", 9 | "linkedin": "", 10 | "skype": "", 11 | "github": "", 12 | "gitlab": "", 13 | "stack-overflow": "", 14 | "twitter": "", 15 | "reddit": "", 16 | "medium": "", 17 | "googlescholar": "" 18 | }, 19 | "about": "I love coding!", 20 | "experience": [ 21 | { 22 | "name": "Company", 23 | "entries": [ 24 | { 25 | "title": "Software Engineer", 26 | "dates": "Jan 2019 - Present", 27 | "location": "Mars", 28 | "description": "Developed blah blah blah" 29 | } 30 | ] 31 | } 32 | ], 33 | "education": [ 34 | { 35 | "name": "School", 36 | "entries": [ 37 | { 38 | "degree": "Bachelor", 39 | "location": "Mercury", 40 | "dates": "2018 - 2019", 41 | "description": "Participated blah blah blah" 42 | } 43 | ] 44 | } 45 | ], 46 | "volunteering": [ 47 | { 48 | "name": "Volunteering Centre", 49 | "entries": [ 50 | { 51 | "title": "Volunteer", 52 | "dates": "Jan 2018 - Dec 2018", 53 | "location": "Venus", 54 | "description": "Helped blah blah blah" 55 | } 56 | ] 57 | } 58 | ], 59 | "skills": [ 60 | "Python", 61 | "Java" 62 | ], 63 | "projects": [ 64 | { 65 | "name": "Project Name", 66 | "dates": "Jan 2018 - Present", 67 | "description": "Created blah blah blah", 68 | "link": "Link to project" 69 | } 70 | ], 71 | "publications": [ 72 | { 73 | "title": "Paper Title", 74 | "date": "Jan 1, 2019", 75 | "publisher": "Nature", 76 | "link": "Link to paper" 77 | } 78 | ], 79 | "honors": [ 80 | { 81 | "title": "Award Title", 82 | "date": "2019", 83 | "location": "Jupiter", 84 | "issuer": "Faculty" 85 | } 86 | ], 87 | "languages": [ 88 | "English" 89 | ] 90 | } -------------------------------------------------------------------------------- /linkedrw/constants.py: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME = "linkedrw" 2 | LATEX_INDENT = " " 3 | HTML_INDENT = " " 4 | CREDENTIALS_FILE = f"~/.{PACKAGE_NAME}/credentials.json" 5 | 6 | CHROME = "chrome" 7 | FIREFOX = "firefox" 8 | SAFARI = "safari" 9 | OPERA = "opera" 10 | DRIVERS = [CHROME, FIREFOX, SAFARI, OPERA] 11 | 12 | NAME = "name" 13 | POSITION = "position" 14 | CONTACT = "contact" 15 | SUMMARY = "about" 16 | EXPERIENCE = "experience" 17 | EDUCATION = "education" 18 | VOLUNTEERING = "volunteering" 19 | SKILLS = "skills" 20 | PROJECTS = "projects" 21 | PUBLICATIONS = "publications" 22 | HONORS = "honors" 23 | LANGUAGES = "languages" 24 | 25 | ADDRESS = "address" 26 | MOBILE = "mobile" 27 | EMAIL = "email" 28 | HOMEPAGE = "homepage" 29 | GITHUB = "github" 30 | LINKEDIN = "linkedin" 31 | GITLAB = "gitlab" 32 | STACKOVERFLOW = "stackoverflow" 33 | TWITTER = "twitter" 34 | SKYPE = "skype" 35 | REDDIT = "reddit" 36 | MEDIUM = "medium" 37 | GOOGLE_SCHOLAR = "googlescholar" 38 | 39 | DATES = "dates" 40 | LOCATION = "location" 41 | DESCRIPTION = "description" 42 | DEGREE = "degree" 43 | TITLE = "title" 44 | ENTRIES = "entries" 45 | DATE = "date" 46 | LINK = "link" 47 | PUBLISHER = "publisher" 48 | ISSUER = "issuer" 49 | 50 | RESUME_TEMPLATE = "templates/resume_template.tex" 51 | PERSONAL_INFO = [ 52 | NAME, 53 | POSITION, 54 | ADDRESS, 55 | MOBILE, 56 | EMAIL, 57 | HOMEPAGE, 58 | GITHUB, 59 | LINKEDIN, 60 | GITLAB, 61 | STACKOVERFLOW, 62 | TWITTER, 63 | SKYPE, 64 | REDDIT, 65 | MEDIUM, 66 | GOOGLE_SCHOLAR, 67 | ] 68 | RESUME_CONTENT = [ 69 | EXPERIENCE, 70 | EDUCATION, 71 | PUBLICATIONS, 72 | HONORS, 73 | PROJECTS, 74 | VOLUNTEERING, 75 | SKILLS, 76 | ] 77 | RESUME_SECTIONS = [EXPERIENCE, EDUCATION, HONORS, PROJECTS, VOLUNTEERING] 78 | SECTION_ITEMS = { 79 | EDUCATION: [DEGREE, NAME, LOCATION, DATES, DESCRIPTION], 80 | EXPERIENCE: [TITLE, NAME, LOCATION, DATES, DESCRIPTION], 81 | HONORS: [TITLE, ISSUER, LOCATION, DATE], 82 | PROJECTS: ["", NAME, "", DATES, DESCRIPTION], 83 | VOLUNTEERING: [TITLE, NAME, LOCATION, DATES, DESCRIPTION], 84 | } 85 | 86 | LATEX_CHARS = { 87 | "&": r"\&", 88 | "%": r"\%", 89 | "$": r"\$", 90 | "#": r"\#", 91 | "_": r"\_", 92 | "{": r"\{", 93 | "}": r"\}", 94 | "~": r"\textasciitilde{}", 95 | "^": r"\^{}", 96 | "\\": r"\textbackslash{}", 97 | "\n": "\\newline%\n", 98 | "-": r"{-}", 99 | "\xA0": "~", # Non-breaking space 100 | "[": r"{[}", 101 | "]": r"{]}", 102 | } 103 | 104 | PORTFOLIO_TEMPLATE = "templates/index_template.html" 105 | PORTFOLIO_SECTIONS = [SUMMARY, EXPERIENCE, EDUCATION, PROJECTS, SKILLS, CONTACT] 106 | CONTACTS = [GITHUB, LINKEDIN, GITLAB, STACKOVERFLOW, TWITTER, REDDIT, MEDIUM] 107 | -------------------------------------------------------------------------------- /tests/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Name", 3 | "position": "Position", 4 | "contact": { 5 | "address": "Address", 6 | "mobile": "Mobile", 7 | "email": "Email", 8 | "homepage": "", 9 | "linkedin": "https://www.linkedin.com/in/linkedin-id", 10 | "skype": "skype-id", 11 | "github": "https://github.com/github-id", 12 | "stackoverflow": "https://stackoverflow.com/users/stack-id/stack-username", 13 | "twitter": "https://twitter.com/twitter-id", 14 | "reddit": "https://www.reddit.com/user/reddit-id", 15 | "medium": "https://medium.com/medium-id", 16 | "googlescholar": "https://scholar.google.com/citations?user=scholar-id" 17 | }, 18 | "about": "About", 19 | "experience": [ 20 | { 21 | "name": "Company", 22 | "entries": [ 23 | { 24 | "title": "Title", 25 | "dates": "Jan 2019 - Present", 26 | "location": "Location", 27 | "description": "Description" 28 | } 29 | ] 30 | } 31 | ], 32 | "education": [ 33 | { 34 | "name": "School", 35 | "entries": [ 36 | { 37 | "degree": "Degree", 38 | "location": "Location", 39 | "dates": "2018 - 2019", 40 | "description": "Description" 41 | } 42 | ] 43 | } 44 | ], 45 | "volunteering": [ 46 | { 47 | "name": "Volunteering Centre", 48 | "entries": [ 49 | { 50 | "title": "Title", 51 | "dates": "Jan 2018 - Dec 2018", 52 | "location": "Location", 53 | "description": "" 54 | } 55 | ] 56 | } 57 | ], 58 | "skills": [ 59 | "Python", 60 | "Java", 61 | "Docker" 62 | ], 63 | "projects": [ 64 | { 65 | "name": "Project", 66 | "dates": "Jan 2018 - Present", 67 | "description": "Description", 68 | "link": "Link" 69 | } 70 | ], 71 | "publications": [ 72 | { 73 | "title": "A rapid and sensitive method for the quantitation of microgram quantities of protein utilizing the principle of protein-dye binding", 74 | "date": "May 7, 1976", 75 | "publisher": "Science Direct", 76 | "link": "https://doi.org/10.1016/0003-2697(76)90527-3" 77 | } 78 | ], 79 | "honors": [ 80 | { 81 | "title": "Award", 82 | "date": "2019", 83 | "location": "Location", 84 | "issuer": "Issuer" 85 | } 86 | ], 87 | "languages": [ 88 | "English" 89 | ] 90 | } -------------------------------------------------------------------------------- /tests/test_resume.py: -------------------------------------------------------------------------------- 1 | import filecmp 2 | import json 3 | import os 4 | import pkg_resources 5 | import tempfile 6 | 7 | from linkedrw.constants import PUBLICATIONS, TITLE, DATE, PUBLISHER, LINK 8 | from linkedrw.linkedr.resume import make_resume_files 9 | from linkedrw.linkedr.publication import make_publication_section, make_references 10 | 11 | RESUME_DIR = "resume" 12 | PROFILE_FILE = "profile.json" 13 | TIMEOUT = 1 14 | 15 | 16 | def test_make_resume_files_full(): 17 | with tempfile.TemporaryDirectory() as dirname: 18 | with open(pkg_resources.resource_filename(__name__, PROFILE_FILE)) as f: 19 | profile = json.load(f) 20 | 21 | make_resume_files(profile, dirname, TIMEOUT) 22 | for filename in os.listdir( 23 | pkg_resources.resource_filename(__name__, RESUME_DIR) 24 | ): 25 | assert ( 26 | filecmp.cmp( 27 | os.path.join(dirname, RESUME_DIR, filename), 28 | pkg_resources.resource_filename( 29 | __name__, f"{RESUME_DIR}/{filename}" 30 | ), 31 | shallow=False, 32 | ) 33 | is True 34 | ) 35 | 36 | 37 | def test_make_publication_section_empty(): 38 | with tempfile.TemporaryDirectory() as dirname: 39 | assert make_publication_section([], dirname) is False 40 | assert os.path.exists(os.path.join(dirname, f"{PUBLICATIONS}.tex")) is False 41 | 42 | 43 | def test_make_resume_files_no_pub(): 44 | with tempfile.TemporaryDirectory() as dirname: 45 | with open(pkg_resources.resource_filename(__name__, PROFILE_FILE)) as f: 46 | profile = json.load(f) 47 | 48 | del profile[PUBLICATIONS] 49 | make_resume_files(profile, dirname, TIMEOUT) 50 | assert ( 51 | os.path.exists(os.path.join(dirname, RESUME_DIR, f"{PUBLICATIONS}.tex")) 52 | is False 53 | ) 54 | 55 | 56 | def test_make_references_no_doi(): 57 | pub = { 58 | TITLE: "A rapid and sensitive method for the quantitation of microgram quantities of protein utilizing the " 59 | "principle of protein-dye binding", 60 | DATE: "May 7, 1976", 61 | PUBLISHER: "Science Direct", 62 | LINK: "", 63 | } 64 | 65 | with tempfile.TemporaryDirectory() as dirname: 66 | assert make_references([pub], dirname) == ["Bradford_1976"] 67 | with open(os.path.join(dirname, "references.bib")) as f: 68 | assert f.read() != "" 69 | 70 | 71 | def test_make_references_not_found(): 72 | pub = {TITLE: "Some random title", DATE: "", PUBLISHER: "", LINK: ""} 73 | 74 | with tempfile.TemporaryDirectory() as dirname: 75 | assert make_references([pub], dirname) == [] 76 | with open(os.path.join(dirname, "references.bib")) as f: 77 | assert f.read() == "" 78 | -------------------------------------------------------------------------------- /linkedrw/linkedr/publication.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from habanero import Crossref, cn 5 | from logbook import Logger 6 | from requests.exceptions import HTTPError 7 | from urllib.parse import urlparse 8 | 9 | from linkedrw.constants import * 10 | 11 | 12 | def make_publication_section(publications, output_dir): 13 | """ 14 | Create publication latex file 15 | Args: 16 | publications: the list of publications 17 | output_dir: the output directory 18 | 19 | Returns: 20 | A bool if there are any publications 21 | """ 22 | if publications: 23 | references = make_references(publications, output_dir) 24 | lines = [f"\\cvsection{{{PUBLICATIONS.title()}}}\n", "\\begin{refsection}"] 25 | 26 | for reference in references: 27 | lines.append(f"{LATEX_INDENT}\\nocite{{{reference}}}") 28 | 29 | lines.append(f"{LATEX_INDENT}\\printbibliography[heading=none]") 30 | lines.append("\\end{refsection}") 31 | 32 | with open(os.path.join(output_dir, f"{PUBLICATIONS}.tex"), "w") as f: 33 | f.write("\n".join(lines)) 34 | 35 | return True 36 | 37 | return False 38 | 39 | 40 | def make_references(publications, output_dir): 41 | """ 42 | Create reference bib file 43 | Args: 44 | publications: the list of publications 45 | output_dir: the output directory 46 | 47 | Returns: 48 | A list of reference identifiers 49 | """ 50 | log = Logger() 51 | cr = Crossref() 52 | lines = [] 53 | references = [] 54 | 55 | for i, publication in enumerate(publications): 56 | log.notice( 57 | f"Querying and formatting {i + 1} out of {len(publications)} publications" 58 | ) 59 | link = publication[LINK] 60 | title = publication[TITLE] 61 | 62 | # Check if it is a DOI url 63 | if link and "doi.org" in link: 64 | doi = urlparse(link).path.strip("/") 65 | 66 | # Extract the DOI using the title 67 | else: 68 | results = cr.works(query_bibliographic=title, limit=1) 69 | if ( 70 | results["message"]["total-results"] == 0 71 | or results["message"]["items"][0]["title"][0].lower() != title.lower() 72 | ): 73 | log.warn(f'Could not find the doi for "{title}"') 74 | 75 | continue 76 | 77 | doi = results["message"]["items"][0]["DOI"] 78 | 79 | try: 80 | reference = cn.content_negotiation(doi) 81 | lines.append(reference) 82 | references.append(re.sub("^@.*{", "", reference.split("\n")[0]).strip(",")) 83 | except HTTPError: 84 | log.warn(f'Could not Create reference for "{title}"') 85 | 86 | with open(os.path.join(output_dir, "references.bib"), "w") as f: 87 | f.write("\n\n".join(lines)) 88 | 89 | return references 90 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/js/scripts.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Title: Dev Portfolio Template 3 | Version: 1.2.1 4 | Last Change: 08/27/2017 5 | Author: Ryan Fitzgerald 6 | Repo: https://github.com/RyanFitzgerald/devportfolio-template 7 | Issues: https://github.com/RyanFitzgerald/devportfolio-template/issues 8 | 9 | Description: This file contains all the scripts associated with the single-page 10 | portfolio website. 11 | */ 12 | 13 | (function($) { 14 | 15 | // Remove no-js class 16 | $('html').removeClass('no-js'); 17 | 18 | // Animate to section when nav is clicked 19 | $('header a').click(function(e) { 20 | 21 | // Treat as normal link if no-scroll class 22 | if ($(this).hasClass('no-scroll')) return; 23 | 24 | e.preventDefault(); 25 | var heading = $(this).attr('href'); 26 | var scrollDistance = $(heading).offset().top; 27 | 28 | $('html, body').animate({ 29 | scrollTop: scrollDistance + 'px' 30 | }, Math.abs(window.pageYOffset - $(heading).offset().top) / 1); 31 | 32 | // Hide the menu once clicked if mobile 33 | if ($('header').hasClass('active')) { 34 | $('header, body').removeClass('active'); 35 | } 36 | }); 37 | 38 | // Scroll to top 39 | $('#to-top').click(function() { 40 | $('html, body').animate({ 41 | scrollTop: 0 42 | }, 500); 43 | }); 44 | 45 | // Scroll to first element 46 | $('#lead-down span').click(function() { 47 | var scrollDistance = $('#lead').next().offset().top; 48 | $('html, body').animate({ 49 | scrollTop: scrollDistance + 'px' 50 | }, 500); 51 | }); 52 | 53 | // Create timeline 54 | $('#experience-timeline').each(function() { 55 | 56 | $this = $(this); // Store reference to this 57 | $userContent = $this.children('div'); // user content 58 | 59 | // Create each timeline block 60 | $userContent.each(function() { 61 | $(this).addClass('vtimeline-content').wrap('
'); 62 | }); 63 | 64 | // Add icons to each block 65 | $this.find('.vtimeline-point').each(function() { 66 | $(this).prepend('
'); 67 | }); 68 | 69 | // Add dates to the timeline if exists 70 | $this.find('.vtimeline-content').each(function() { 71 | var date = $(this).data('date'); 72 | if (date) { // Prepend if exists 73 | $(this).parent().prepend(''+date+''); 74 | } 75 | }); 76 | 77 | }); 78 | 79 | // Open mobile menu 80 | $('#mobile-menu-open').click(function() { 81 | $('header, body').addClass('active'); 82 | }); 83 | 84 | // Close mobile menu 85 | $('#mobile-menu-close').click(function() { 86 | $('header, body').removeClass('active'); 87 | }); 88 | 89 | // Load additional projects 90 | $('#view-more-projects').click(function(e){ 91 | e.preventDefault(); 92 | $(this).fadeOut(300, function() { 93 | $('#more-projects').fadeIn(300); 94 | }); 95 | }); 96 | 97 | })(jQuery); 98 | -------------------------------------------------------------------------------- /linkedrw/templates/resume_template.tex: -------------------------------------------------------------------------------- 1 | %!TEX TS-program = xelatex 2 | %!TEX encoding = UTF-8 Unicode 3 | % Awesome CV LaTeX Template for CV/Resume 4 | % 5 | % This template has been downloaded from: 6 | % https://github.com/posquit0/Awesome-CV 7 | % 8 | % Author: 9 | % Claud D. Park 10 | % http://www.posquit0.com 11 | % 12 | % Template license: 13 | % CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) 14 | % 15 | 16 | 17 | %------------------------------------------------------------------------------- 18 | % CONFIGURATIONS 19 | %------------------------------------------------------------------------------- 20 | % A4 paper size by default, use 'letterpaper' for US letter 21 | \documentclass[11pt, a4paper]{awesome-cv} 22 | 23 | % Configure page margins with geometry 24 | \geometry{left=1.4cm, top=.8cm, right=1.4cm, bottom=1.8cm, footskip=.5cm} 25 | 26 | % Specify the location of the included fonts 27 | \fontdir[fonts/] 28 | 29 | % Color for highlights 30 | % Awesome Colors: awesome-emerald, awesome-skyblue, awesome-red, awesome-pink, awesome-orange 31 | % awesome-nephritis, awesome-concrete, awesome-darknight 32 | \colorlet{awesome}{awesome-darknight} 33 | % Uncomment if you would like to specify your own color 34 | % \definecolor{awesome}{HTML}{CA63A8} 35 | 36 | % Colors for text 37 | % Uncomment if you would like to specify your own color 38 | % \definecolor{darktext}{HTML}{414141} 39 | % \definecolor{text}{HTML}{333333} 40 | % \definecolor{graytext}{HTML}{5D5D5D} 41 | % \definecolor{lighttext}{HTML}{999999} 42 | 43 | % Set false if you don't want to highlight section with awesome color 44 | \setbool{acvSectionColorHighlight}{true} 45 | 46 | % If you would like to change the social information separator from a pipe (|) to something else 47 | \renewcommand{\acvHeaderSocialSep}{\quad\textbar\quad} 48 | 49 | 50 | %------------------------------------------------------------------------------- 51 | % PERSONAL INFORMATION 52 | % Comment any of the lines below if they are not required 53 | %------------------------------------------------------------------------------- 54 | % Available options: circle|rectangle,edge/noedge,left/right 55 | % \photo{profile.png} 56 | % personal-info-here 57 | % \extrainfo{extra informations} 58 | 59 | % \quote{``Be the change that you want to see in the world."} 60 | 61 | %------------------------------------------------------------------------------- 62 | % BIBLIOGRAPHY 63 | %------------------------------------------------------------------------------- 64 | % \addbibresource{references.bib} 65 | 66 | %------------------------------------------------------------------------------- 67 | \begin{document} 68 | 69 | % Print the header with above personal informations 70 | % Give optional argument to change alignment(C: center, L: left, R: right) 71 | \makecvheader 72 | 73 | % Print the footer with 3 arguments(,
, ) 74 | % Leave any of these blank if they are not needed 75 | \makecvfooter 76 | 77 | 78 | %------------------------------------------------------------------------------- 79 | % CV/RESUME CONTENT 80 | % Each section is imported separately, open each file in turn to modify content 81 | %------------------------------------------------------------------------------- 82 | % resume-content-here 83 | 84 | %------------------------------------------------------------------------------- 85 | \end{document} 86 | -------------------------------------------------------------------------------- /tests/resume/resume.tex: -------------------------------------------------------------------------------- 1 | %!TEX TS-program = xelatex 2 | %!TEX encoding = UTF-8 Unicode 3 | % Awesome CV LaTeX Template for CV/Resume 4 | % 5 | % This template has been downloaded from: 6 | % https://github.com/posquit0/Awesome-CV 7 | % 8 | % Author: 9 | % Claud D. Park 10 | % http://www.posquit0.com 11 | % 12 | % Template license: 13 | % CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) 14 | % 15 | 16 | 17 | %------------------------------------------------------------------------------- 18 | % CONFIGURATIONS 19 | %------------------------------------------------------------------------------- 20 | % A4 paper size by default, use 'letterpaper' for US letter 21 | \documentclass[11pt, a4paper]{awesome-cv} 22 | 23 | % Configure page margins with geometry 24 | \geometry{left=1.4cm, top=.8cm, right=1.4cm, bottom=1.8cm, footskip=.5cm} 25 | 26 | % Specify the location of the included fonts 27 | \fontdir[fonts/] 28 | 29 | % Color for highlights 30 | % Awesome Colors: awesome-emerald, awesome-skyblue, awesome-red, awesome-pink, awesome-orange 31 | % awesome-nephritis, awesome-concrete, awesome-darknight 32 | \colorlet{awesome}{awesome-darknight} 33 | % Uncomment if you would like to specify your own color 34 | % \definecolor{awesome}{HTML}{CA63A8} 35 | 36 | % Colors for text 37 | % Uncomment if you would like to specify your own color 38 | % \definecolor{darktext}{HTML}{414141} 39 | % \definecolor{text}{HTML}{333333} 40 | % \definecolor{graytext}{HTML}{5D5D5D} 41 | % \definecolor{lighttext}{HTML}{999999} 42 | 43 | % Set false if you don't want to highlight section with awesome color 44 | \setbool{acvSectionColorHighlight}{true} 45 | 46 | % If you would like to change the social information separator from a pipe (|) to something else 47 | \renewcommand{\acvHeaderSocialSep}{\quad\textbar\quad} 48 | 49 | 50 | %------------------------------------------------------------------------------- 51 | % PERSONAL INFORMATION 52 | % Comment any of the lines below if they are not required 53 | %------------------------------------------------------------------------------- 54 | % Available options: circle|rectangle,edge/noedge,left/right 55 | % \photo{profile.png} 56 | \name{Name}{} 57 | \position{Position} 58 | \address{Address} 59 | \mobile{Mobile} 60 | \email{Email} 61 | % \homepage{} 62 | \github{github-id} 63 | \linkedin{linkedin-id} 64 | % \gitlab{} 65 | \stackoverflow{stack-id}{stack-username} 66 | \twitter{@twitter-id} 67 | \skype{skype-id} 68 | \reddit{reddit-id} 69 | \medium{medium-id} 70 | \googlescholar{scholar-id}{} 71 | % \extrainfo{extra informations} 72 | 73 | % \quote{``Be the change that you want to see in the world."} 74 | 75 | %------------------------------------------------------------------------------- 76 | % BIBLIOGRAPHY 77 | %------------------------------------------------------------------------------- 78 | \addbibresource{references.bib} 79 | 80 | %------------------------------------------------------------------------------- 81 | \begin{document} 82 | 83 | % Print the header with above personal informations 84 | % Give optional argument to change alignment(C: center, L: left, R: right) 85 | \makecvheader 86 | 87 | % Print the footer with 3 arguments(,
, ) 88 | % Leave any of these blank if they are not needed 89 | \makecvfooter{\today}{Name~~~·~~~Resume}{\thepage} 90 | 91 | 92 | %------------------------------------------------------------------------------- 93 | % CV/RESUME CONTENT 94 | % Each section is imported separately, open each file in turn to modify content 95 | %------------------------------------------------------------------------------- 96 | \input{experience.tex} 97 | \input{education.tex} 98 | \input{publications.tex} 99 | \input{honors.tex} 100 | \input{projects.tex} 101 | \input{volunteering.tex} 102 | \input{skills.tex} 103 | 104 | %------------------------------------------------------------------------------- 105 | \end{document} -------------------------------------------------------------------------------- /linkedrw/linkedr/section.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from linkedrw.constants import * 4 | from linkedrw.utils import escape_latex 5 | 6 | 7 | def make_resume_section(profile, section, output_dir): 8 | """ 9 | Create the latex file for the given section 10 | Args: 11 | profile: the dict of the profile 12 | section: the section to Create 13 | output_dir: the output directory 14 | 15 | Returns: 16 | None 17 | """ 18 | if section in SECTION_ITEMS and section in profile and profile[section]: 19 | title = "Honors \\& Awards" if section == HONORS else section.title() 20 | cv_type = "cvhonors" if section == HONORS else "cventries" 21 | lines = [f"\\cvsection{{{title}}}\n", f"\\begin{{{cv_type}}}"] 22 | 23 | if section in (EDUCATION, EXPERIENCE, VOLUNTEERING): 24 | lines += make_grouped_section(profile, section) 25 | else: 26 | lines += make_ungrouped_section(profile, section) 27 | 28 | lines.append(f"\\end{{{cv_type}}}") 29 | with open(os.path.join(output_dir, f"{section}.tex"), "w") as f: 30 | f.write("\n".join(lines)) 31 | 32 | 33 | def make_grouped_section(profile, section): 34 | """ 35 | Create the lines for grouped entries 36 | Args: 37 | profile: the dict of the profile 38 | section: the section 39 | 40 | Returns: 41 | A list of lines for the given section 42 | """ 43 | lines = [] 44 | for entry in profile[section]: 45 | name = entry[NAME] 46 | for i, item in enumerate(entry[ENTRIES]): 47 | lines.append(f"{LATEX_INDENT}\\cventry") 48 | for key in SECTION_ITEMS[section]: 49 | if key == NAME and i == 0: 50 | lines.append(f"{LATEX_INDENT * 2}{{{escape_latex(name)}}} % {NAME}") 51 | elif key == DESCRIPTION: 52 | lines += get_descriptions(item) 53 | elif key and key != NAME: 54 | lines.append( 55 | f"{LATEX_INDENT * 2}{{{escape_latex(item[key])}}} % {key}" 56 | ) 57 | else: 58 | lines.append(f"{LATEX_INDENT * 2}{{}}") 59 | 60 | return lines 61 | 62 | 63 | def make_ungrouped_section(profile, section): 64 | """ 65 | Create the lines for ungrouped entries 66 | Args: 67 | profile: the dict of the profile 68 | section: the section 69 | 70 | Returns: 71 | A list of lines for the given section 72 | """ 73 | lines = [] 74 | for entry in profile[section]: 75 | if section == HONORS: 76 | lines.append(f"{LATEX_INDENT}\\cvhonor") 77 | else: 78 | lines.append(f"{LATEX_INDENT}\\cventry") 79 | 80 | for key in SECTION_ITEMS[section]: 81 | if key == DESCRIPTION: 82 | lines += get_descriptions(entry) 83 | elif key: 84 | lines.append( 85 | f"{LATEX_INDENT * 2}{{{escape_latex(entry[key])}}} % {key}" 86 | ) 87 | else: 88 | lines.append(f"{LATEX_INDENT * 2}{{}}") 89 | 90 | return lines 91 | 92 | 93 | def get_descriptions(item): 94 | """ 95 | Create the lines for the description 96 | Args: 97 | item: the item 98 | 99 | Returns: 100 | A list of lines for the description 101 | """ 102 | lines = [] 103 | if item[DESCRIPTION]: 104 | lines.append(f"{LATEX_INDENT * 2}{{") 105 | lines.append(f"{LATEX_INDENT * 3}\\begin{{cvitems}}") 106 | 107 | for description in item[DESCRIPTION].split("\n"): 108 | description = description.strip("-").strip() 109 | if description: 110 | description = escape_latex(description) 111 | lines.append(f"{LATEX_INDENT * 4}\\item{{{description}}}") 112 | 113 | lines.append(f"{LATEX_INDENT * 3}\\end{{cvitems}}") 114 | lines.append(f"{LATEX_INDENT * 2}}}\n") 115 | else: 116 | lines.append(f"{LATEX_INDENT * 2}{{}} % {DESCRIPTION}\n") 117 | 118 | return lines 119 | -------------------------------------------------------------------------------- /linkedrw/templates/index_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | My Portfolio 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | Close 23 |
24 | 27 |
28 | 29 | 30 |
31 |
32 |

name-here

33 |

title-here

34 | Download Resume 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 |
45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 |

About Me

54 |
55 |
56 |

57 | summary-here 58 |

59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |

Experience

67 |
68 | experience-here 69 |
70 |
71 | 72 | 73 |
74 |

Education

75 | education-here 76 |
77 | 78 | 79 |
80 |

Projects

81 |
82 |
83 | projects-here 84 |
85 |
86 |
87 | 88 | 89 |
90 |

Skills

91 |
    92 | skills-here 93 |
94 |
95 | 96 | 97 |
98 |

Get in Touch

99 |
100 |
101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 |
109 | 110 | 111 |
112 |
113 |
114 | 119 |
120 | 121 | 122 | 123 |
124 | 129 | 130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /tests/website/empty/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | My Portfolio 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | Close 23 |
24 | 29 |
30 | 31 | 32 |
33 |
34 |

name-here

35 |

title-here

36 | Download Resume 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 |

Get in Touch

96 |
97 |
98 | 99 | 100 | 101 | 102 |
103 |
104 | 105 |
106 | 107 | 108 |
109 |
110 |
111 | 116 |
117 | 118 | 119 | 120 |
121 | 122 | 123 | 124 | 125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /linkedrw/utils/helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkg_resources 3 | import shutil 4 | 5 | from selenium.common.exceptions import NoSuchElementException 6 | 7 | from linkedrw.constants import LATEX_CHARS 8 | 9 | 10 | def copy_files(mod_name, dir_name, output_dir): 11 | """ 12 | Copy files under dir_name to output_dir 13 | Args: 14 | mod_name: the module name 15 | dir_name: the directory name of the files to be copied 16 | output_dir: the directory name for the files to be copied to 17 | 18 | Returns: 19 | None 20 | """ 21 | files = pkg_resources.resource_filename(mod_name, dir_name) 22 | for filename in os.listdir(files): 23 | full_filename = os.path.join(files, filename) 24 | if os.path.isdir(full_filename): 25 | try: 26 | shutil.copytree(full_filename, os.path.join(output_dir, filename)) 27 | except FileExistsError: 28 | continue 29 | else: 30 | shutil.copy(full_filename, output_dir) 31 | 32 | 33 | def escape_latex(s): 34 | """ 35 | Escape LaTeX special characters 36 | Args: 37 | s: the string 38 | 39 | Returns: 40 | a string with escaped LaTeX special characters 41 | """ 42 | return "".join(LATEX_CHARS.get(c, c) for c in s) 43 | 44 | 45 | def get_span_text(element, name): 46 | """ 47 | Scrape text inside the span element 48 | Args: 49 | element: the element containing the text 50 | name: the class name 51 | 52 | Returns: 53 | A string of text 54 | """ 55 | try: 56 | return ( 57 | element.find_element_by_css_selector(name) 58 | .find_elements_by_tag_name("span")[1] 59 | .text.replace("–", "-") 60 | ) 61 | except NoSuchElementException: 62 | return "" 63 | 64 | 65 | def get_optional_text(element, name, is_span=True): 66 | """ 67 | Scrape text that may or may not exist 68 | Args: 69 | element: the element containing the text 70 | name: the class name 71 | is_span: the bool if the text is wrapped inside span tags 72 | 73 | Returns: 74 | A string of text 75 | """ 76 | text = "" 77 | try: 78 | if is_span: 79 | text = get_span_text(element, name) 80 | else: 81 | text = element.find_element_by_css_selector(name).text.replace("–", "-") 82 | except NoSuchElementException: 83 | pass 84 | 85 | return text 86 | 87 | 88 | def get_optional_text_replace(element, name, text): 89 | """ 90 | Scrape text that may or may not exist and remove a specific text 91 | Args: 92 | element: the element containing the text 93 | name: the class name 94 | text: the text to be removed 95 | 96 | Returns: 97 | A string of text 98 | """ 99 | try: 100 | return element.find_element_by_class_name(name).text.replace(text, "").strip() 101 | except NoSuchElementException: 102 | return "" 103 | 104 | 105 | def get_description(element, name): 106 | """ 107 | Scrape the description 108 | Args: 109 | element: the element containing the description 110 | name: the class name 111 | 112 | Returns: 113 | A string of description 114 | """ 115 | try: 116 | section = element.find_element_by_css_selector(name) 117 | btn_section = section.find_elements_by_class_name("lt-line-clamp__ellipsis") 118 | 119 | # Check if there is a more button 120 | if not btn_section or "lt-line-clamp__ellipsis--dummy" in btn_section[ 121 | 0 122 | ].get_attribute("class"): 123 | description = section.text 124 | else: 125 | btn_section[0].find_element_by_class_name("lt-line-clamp__more").click() 126 | description = section.find_element_by_class_name( 127 | "lt-line-clamp__raw-line" 128 | ).text 129 | except NoSuchElementException: 130 | description = "" 131 | 132 | description = description.replace("•", "-") 133 | 134 | return description 135 | 136 | 137 | def get_accomplishment_link(element): 138 | """ 139 | Scrape the accomplishment link 140 | Args: 141 | element: the element containing the link 142 | 143 | Returns: 144 | A string of link 145 | """ 146 | try: 147 | return element.find_element_by_class_name( 148 | "pv-accomplishment-entity__external-source" 149 | ).get_attribute("href") 150 | except NoSuchElementException: 151 | return "" 152 | 153 | 154 | def make_dir(dir_name): 155 | """ 156 | Make directory 157 | Args: 158 | dir_name: the directory name to be created 159 | 160 | Returns: 161 | None 162 | """ 163 | try: 164 | os.mkdir(dir_name) 165 | except FileExistsError: 166 | pass 167 | 168 | 169 | def scroll_to_elem(driver, by, value, align="true"): 170 | elem = driver.find_element(by, value) 171 | script = ( 172 | f"arguments[0].scrollIntoView({align});" 173 | "var scrollTimeout;" 174 | "addEventListener('scroll', function(e) {" 175 | "clearTimeout(scrollTimeout);" 176 | "scrollTimeout = setTimeout(function() {}, 1000);" 177 | "});" 178 | ) 179 | driver.execute_script(script, elem) 180 | 181 | return elem 182 | -------------------------------------------------------------------------------- /linkedrw/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import logbook 4 | import os 5 | import sys 6 | 7 | from getpass import getpass 8 | from logbook import Logger, StreamHandler 9 | 10 | from linkedrw.constants import PACKAGE_NAME, CREDENTIALS_FILE, CHROME, DRIVERS, FIREFOX 11 | from linkedrw.utils import make_dir 12 | from linkedrw.scraper import scrape 13 | from linkedrw.linkedr import make_resume_files 14 | from linkedrw.linkedw import make_website_files 15 | 16 | 17 | def main(): 18 | parser = argparse.ArgumentParser( 19 | description="Creates a resume and a personal website based on your LinkedIn profile" 20 | ) 21 | parser.set_defaults(method=run) 22 | 23 | parser.add_argument("--email", "-e", help="Your LinkedIn login email") 24 | parser.add_argument("--password", "-p", help="Your LinkedIn login password") 25 | parser.add_argument( 26 | "--keep_creds", 27 | "-k", 28 | action="store_true", 29 | help=f"Store LinkedIn login credentials under {CREDENTIALS_FILE}", 30 | ) 31 | parser.add_argument( 32 | "--output_dir", 33 | "-o", 34 | default=".", 35 | help="The output directory (default: current directory)", 36 | ) 37 | parser.add_argument( 38 | "--scrape_only", "-s", action="store_true", help="Only scrape LinkedIn profile" 39 | ) 40 | parser.add_argument( 41 | "--resume_only", "-r", action="store_true", help="Only create resume" 42 | ) 43 | parser.add_argument( 44 | "--website_only", "-w", action="store_true", help="Only create personal website" 45 | ) 46 | parser.add_argument( 47 | "--profile", "-j", dest="profile_file", help="The profile json file" 48 | ) 49 | parser.add_argument( 50 | "--driver", 51 | "-d", 52 | default=CHROME, 53 | help=f'The web driver: {", ".join(DRIVERS)} (default: %(default)s)', 54 | ) 55 | parser.add_argument( 56 | "--driver_path", "-dp", help="The executable path of the web driver" 57 | ) 58 | parser.add_argument( 59 | "--timeout", 60 | "-t", 61 | type=int, 62 | default=10, 63 | help="The timeout value (default: %(default)s)", 64 | ) 65 | 66 | args = parser.parse_args() 67 | args.method(**vars(args)) 68 | 69 | 70 | def run( 71 | driver, 72 | email, 73 | password, 74 | keep_creds, 75 | output_dir, 76 | scrape_only, 77 | resume_only, 78 | website_only, 79 | profile_file, 80 | timeout, 81 | driver_path, 82 | **kwargs, 83 | ): 84 | # Setup logging 85 | logbook.set_datetime_format("local") 86 | format_string = ( 87 | "[{record.time:%Y-%m-%d %H:%M:%S}] {record.level_name}: {record.message}" 88 | ) 89 | StreamHandler(sys.stdout, format_string=format_string).push_application() 90 | log = Logger() 91 | 92 | # Create output directory 93 | make_dir(output_dir) 94 | 95 | # Check if user has provided the profile json file 96 | if profile_file is None: 97 | if driver.lower() not in DRIVERS: 98 | raise ValueError( 99 | f'Browser driver has to be one of these: {", ".join(DRIVERS)}' 100 | ) 101 | 102 | # Check if credentials file exists 103 | credentials_file = os.path.expanduser(CREDENTIALS_FILE) 104 | if os.path.exists(credentials_file): 105 | with open(credentials_file) as f: 106 | credentials = json.load(f) 107 | email = credentials["email"] 108 | password = credentials["password"] 109 | else: 110 | if email is None: 111 | email = input("Enter your LinkedIn login email: ") 112 | if password is None: 113 | password = getpass("Enter your LinkedIn login password: ") 114 | 115 | log.notice("Scraping LinkedIn profile") 116 | if driver not in [CHROME, FIREFOX]: 117 | log.notice("Please keep the browser window on top") 118 | 119 | profile = scrape( 120 | driver.lower(), driver_path, email, password, output_dir, timeout 121 | ) 122 | 123 | if keep_creds: 124 | store_creds(email, password, credentials_file) 125 | else: 126 | with open(profile_file) as f: 127 | profile = json.load(f) 128 | 129 | if not scrape_only: 130 | if resume_only: 131 | make_resume_files(profile, output_dir, timeout) 132 | elif website_only: 133 | make_website_files(profile, output_dir) 134 | else: 135 | make_resume_files(profile, output_dir, timeout) 136 | make_website_files(profile, output_dir) 137 | 138 | 139 | def store_creds(email, password, creds_file): 140 | """ 141 | Store login credentials 142 | Args: 143 | email: the LinkedIn login email 144 | password: the LinkedIn login password 145 | creds_file: the credentials file to store the login credentials 146 | 147 | Returns: 148 | None 149 | """ 150 | log = Logger() 151 | log.warn( 152 | f"It is highly NOT recommended to keep your login credentials, " 153 | f"you can always remove the file {CREDENTIALS_FILE} to remove them" 154 | ) 155 | 156 | make_dir(os.path.expanduser(f"~/.{PACKAGE_NAME}")) 157 | credentials = {"email": email, "password": password} 158 | 159 | with open(creds_file, "w") as f: 160 | json.dump(credentials, f) 161 | -------------------------------------------------------------------------------- /linkedrw/scraper/scrape.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | 5 | from selenium import webdriver 6 | from selenium.common.exceptions import NoSuchElementException 7 | from selenium.webdriver.chrome.options import Options as ChromeOptions 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 10 | from selenium.webdriver.support import expected_conditions as ec 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | 13 | from linkedrw.exceptions import LoginError 14 | from linkedrw.constants import * 15 | from linkedrw.scraper.accomplishment import get_accomplishment_details 16 | from linkedrw.scraper.background import get_background_details 17 | from linkedrw.scraper.personal import get_personal_details 18 | 19 | 20 | def scrape(browser_driver, driver_path, email, password, output_dir, timeout): 21 | if browser_driver == CHROME: 22 | options = ChromeOptions() 23 | options.add_argument("--headless") 24 | options.add_argument("--window-size=1920,1080") 25 | 26 | if driver_path is None: 27 | driver = webdriver.Chrome(options=options) 28 | else: 29 | driver = webdriver.Chrome(executable_path=driver_path, options=options) 30 | elif browser_driver == FIREFOX: 31 | options = FirefoxOptions() 32 | options.add_argument("--headless") 33 | 34 | if driver_path is None: 35 | driver = webdriver.Firefox(options=options) 36 | else: 37 | driver = webdriver.Firefox(executable_path=driver_path, options=options) 38 | elif browser_driver == SAFARI: 39 | if driver_path is None: 40 | driver = webdriver.Safari() 41 | else: 42 | driver = webdriver.Safari(executable_path=driver_path) 43 | elif browser_driver == OPERA: 44 | if driver_path is None: 45 | driver = webdriver.Opera() 46 | else: 47 | driver = webdriver.Opera(executable_path=driver_path) 48 | else: 49 | raise ValueError(f'Browser driver has to be one of these: {", ".join(DRIVERS)}') 50 | 51 | # Login to LinkedIn 52 | driver.get("https://www.linkedin.com/login/") 53 | driver.find_element_by_id("username").send_keys(email) 54 | driver.find_element_by_id("password").send_keys(password) 55 | driver.find_element_by_class_name("login__form_action_container").submit() 56 | 57 | # Check if login is successful 58 | html = driver.page_source.lower() 59 | if any( 60 | x in html 61 | for x in ["we don't recognize that email", "that's not the right password"] 62 | ): 63 | driver.quit() 64 | 65 | raise LoginError("Invalid login credentials") 66 | 67 | # Skip adding a phone number 68 | try: 69 | driver.find_element_by_css_selector(".ember-view.cp-add-phone") 70 | driver.find_element_by_class_name("secondary-action").click() 71 | except NoSuchElementException: 72 | pass 73 | 74 | # Navigate to profile page 75 | elem = WebDriverWait(driver, timeout).until( 76 | ec.presence_of_element_located( 77 | (By.CSS_SELECTOR, ".tap-target.block.link-without-hover-visited.ember-view") 78 | ) 79 | ) 80 | elem.click() 81 | WebDriverWait(driver, timeout).until( 82 | ec.presence_of_element_located((By.ID, "oc-background-section")) 83 | ) 84 | 85 | # Scrape profile 86 | profile = { 87 | NAME: get_personal_details(driver, NAME), 88 | POSITION: get_personal_details(driver, POSITION), 89 | CONTACT: get_personal_details(driver, CONTACT, timeout), 90 | SUMMARY: get_personal_details(driver, SUMMARY), 91 | EXPERIENCE: get_background_details( 92 | driver, By.ID, "experience-section", EXPERIENCE 93 | ), 94 | EDUCATION: get_background_details( 95 | driver, By.ID, "education-section", EDUCATION 96 | ), 97 | VOLUNTEERING: get_background_details( 98 | driver, 99 | By.CSS_SELECTOR, 100 | ".pv-profile-section.volunteering-section.ember-view", 101 | VOLUNTEERING, 102 | ), 103 | SKILLS: get_background_details( 104 | driver, 105 | By.CSS_SELECTOR, 106 | ".pv-profile-section.pv-skill-categories-section.artdeco-container-card.ember-view", 107 | SKILLS, 108 | ), 109 | PROJECTS: get_accomplishment_details(driver, PROJECTS), 110 | PUBLICATIONS: get_accomplishment_details(driver, PUBLICATIONS), 111 | HONORS: get_accomplishment_details(driver, HONORS), 112 | LANGUAGES: get_accomplishment_details(driver, LANGUAGES), 113 | } 114 | 115 | driver.quit() 116 | with open(os.path.join(output_dir, "profile.json"), "w") as f: 117 | json.dump(profile, f, indent=4) 118 | 119 | return profile 120 | 121 | 122 | if __name__ == "__main__": 123 | parser = argparse.ArgumentParser(description="Scrape your LinkedIn profile") 124 | parser.set_defaults(method=scrape) 125 | 126 | parser.add_argument("email", help="Your LinkedIn login email") 127 | parser.add_argument("password", help="Your LinkedIn login password") 128 | parser.add_argument( 129 | "--output_dir", 130 | "-o", 131 | default=".", 132 | help="The output directory (default: current directory)", 133 | ) 134 | 135 | args = parser.parse_args() 136 | args.method(**vars(args)) 137 | -------------------------------------------------------------------------------- /tests/website/no_date/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | My Portfolio 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | Close 23 |
24 | 32 |
33 | 34 | 35 |
36 |
37 |

name-here

38 |

title-here

39 | Download Resume 40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |

Education

78 |
79 |

Name

80 | 2018 - 2019 81 |

Degree 2

82 |
    83 |
  • Description 2 84 |
85 |
86 | 87 |
88 |

Name

89 | 90 |

Degree 1

91 |
    92 |
  • Description 1 93 |
94 |
95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 |

Get in Touch

117 |
118 |
119 | 120 | 121 | 122 | 123 |
124 |
125 | 126 |
127 | 128 | 129 |
130 |
131 |
132 | 137 |
138 | 139 | 140 | 141 |
142 | 143 | 144 | 145 | 146 | 147 |
148 |
149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /linkedrw/scraper/accomplishment.py: -------------------------------------------------------------------------------- 1 | from selenium.common.exceptions import NoSuchElementException 2 | from selenium.webdriver.common.by import By 3 | 4 | from linkedrw.constants import * 5 | from linkedrw.utils import ( 6 | get_optional_text, 7 | get_optional_text_replace, 8 | get_description, 9 | get_accomplishment_link, 10 | scroll_to_elem, 11 | ) 12 | 13 | 14 | def get_accomplishment_details(driver, section_type): 15 | """ 16 | Scrape accomplishment details 17 | Args: 18 | driver: the selenium driver 19 | section_type: the section type 20 | 21 | Returns: 22 | A list of details of all the items under the given section 23 | """ 24 | # Check if the section exists 25 | try: 26 | section = driver.find_element_by_css_selector( 27 | f".accordion-panel.pv-profile-section.pv-accomplishments-block.{section_type}.ember-view" 28 | ) 29 | if "display: none" in section.get_attribute("style"): 30 | return [] 31 | except NoSuchElementException: 32 | return [] 33 | 34 | if section_type in (PROJECTS, PUBLICATIONS, HONORS): 35 | # Expand the section 36 | btn = scroll_to_elem( 37 | driver, 38 | By.XPATH, 39 | f"//button[@aria-controls='{section_type}-expandable-content']", 40 | align="false", 41 | ) 42 | btn.click() 43 | section = driver.find_element_by_css_selector( 44 | f".accordion-panel.pv-profile-section.pv-accomplishments-block.{section_type}." 45 | f"pv-accomplishments-block--expanded.ember-view" 46 | ) 47 | 48 | # Show all items 49 | try: 50 | section.find_element_by_xpath( 51 | f"//button[@aria-controls='{section_type}-accomplishment-list']" 52 | ).click() 53 | ul = section.find_element_by_css_selector( 54 | ".pv-accomplishments-block__list.pv-accomplishments-block__list--has-more" 55 | ) 56 | except NoSuchElementException: 57 | ul = section.find_element_by_class_name("pv-accomplishments-block__list ") 58 | 59 | if section_type == PROJECTS: 60 | return get_projects(ul) 61 | elif section_type == PUBLICATIONS: 62 | return get_publications(ul) 63 | elif section_type == HONORS: 64 | return get_honors(ul) 65 | elif section_type == LANGUAGES: 66 | return get_languages(section) 67 | 68 | 69 | def get_projects(ul): 70 | """ 71 | Scrape projects details 72 | Args: 73 | ul: the ul element 74 | 75 | Returns: 76 | A list of details of all projects 77 | """ 78 | projects = [] 79 | for li in ul.find_elements_by_tag_name("li"): 80 | name = ( 81 | li.find_element_by_class_name("pv-accomplishment-entity__title") 82 | .text.replace("Project name", "") 83 | .strip() 84 | ) 85 | dates = get_optional_text( 86 | li, 87 | ".pv-accomplishment-entity__date.pv-accomplishment-entity__subtitle", 88 | is_span=False, 89 | ) 90 | description = get_description( 91 | li, ".pv-accomplishment-entity__description.t-14" 92 | ).lstrip("Project description\n") 93 | link = get_accomplishment_link(li) 94 | 95 | projects.append( 96 | {NAME: name, DATES: dates, DESCRIPTION: description, LINK: link} 97 | ) 98 | 99 | return projects 100 | 101 | 102 | def get_publications(ul): 103 | """ 104 | Scrape publications details 105 | Args: 106 | ul: the ul element 107 | 108 | Returns: 109 | A list of details of all publications 110 | """ 111 | publications = [] 112 | for li in ul.find_elements_by_tag_name("li"): 113 | title = ( 114 | li.find_element_by_class_name("pv-accomplishment-entity__title") 115 | .text.replace("publication title", "") 116 | .strip() 117 | ) 118 | date = get_optional_text_replace( 119 | li, "pv-accomplishment-entity__date", "publication date" 120 | ) 121 | publisher = get_optional_text_replace( 122 | li, "pv-accomplishment-entity__publisher", "publication description" 123 | ) 124 | link = get_accomplishment_link(li) 125 | 126 | publications.append( 127 | {TITLE: title, DATE: date, PUBLISHER: publisher, LINK: link} 128 | ) 129 | 130 | return publications 131 | 132 | 133 | def get_honors(ul): 134 | """ 135 | Scrape honors/awards details 136 | Args: 137 | ul: the ul element 138 | 139 | Returns: 140 | A list of details of all honors/awards 141 | """ 142 | awards = [] 143 | for li in ul.find_elements_by_tag_name("li"): 144 | title = ( 145 | li.find_element_by_class_name("pv-accomplishment-entity__title") 146 | .text.replace("honor title", "") 147 | .strip() 148 | ) 149 | date = get_optional_text_replace( 150 | li, "pv-accomplishment-entity__date", "honor date" 151 | ) 152 | issuer = get_optional_text_replace( 153 | li, "pv-accomplishment-entity__issuer", "honor issuer" 154 | ) 155 | 156 | awards.append({TITLE: title, DATE: date, LOCATION: "", ISSUER: issuer}) 157 | 158 | return awards 159 | 160 | 161 | def get_languages(section): 162 | """ 163 | Scrape languages 164 | Args: 165 | section: the languages section 166 | 167 | Returns: 168 | A list of languages 169 | """ 170 | return [x.text for x in section.find_elements_by_tag_name("li")] 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ⚠️ This app is no longer maintained. ⚠️ 2 | 3 | # LinkedRW 4 | 5 | [![PyPi Package Version](https://img.shields.io/pypi/v/linkedrw.svg)](https://pypi.org/project/linkedrw/) 6 | [![Supported Python Versions](https://img.shields.io/pypi/pyversions/linkedrw.svg)](https://pypi.org/project/linkedrw/) 7 | [![MIT License](https://img.shields.io/pypi/l/linkedrw.svg)](https://github.com/zeshuaro/LinkedRW/blob/master/LICENSE) 8 | 9 | [![Build Status](https://travis-ci.com/zeshuaro/LinkedRW.svg?branch=master)](https://travis-ci.com/zeshuaro/LinkedRW) 10 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/04b86b6463f749f79378ca580257fbb7)](https://www.codacy.com/app/zeshuaro/LinkedRW?utm_source=github.com&utm_medium=referral&utm_content=zeshuaro/LinkedRW&utm_campaign=Badge_Grade) 11 | [![codecov](https://codecov.io/gh/zeshuaro/linkedRW/branch/master/graph/badge.svg)](https://codecov.io/gh/zeshuaro/linkedRW) 12 | 13 | A simple CLI for you to create your resume using the [Awesome CV](https://github.com/posquit0/Awesome-CV) template, 14 | and your personal website using the [Dev Portfolio](https://github.com/RyanFitzgerald/devportfolio) template, 15 | based on your LinkedIn profile. 16 | 17 | ## Installation 18 | 19 | Install through pip: 20 | 21 | ```bash 22 | pip install linkedrw 23 | ``` 24 | 25 | You will also need to download a web driver. You can either put it in path (e.g. `/usr/local/bin/`) or specify it by using the `-dp/--driver_path` option, `linkedrw` supports the following: 26 | 27 | * [Chrome Driver](https://sites.google.com/a/chromium.org/chromedriver/downloads) 28 | * [Firefox Driver](https://github.com/mozilla/geckodriver/releases) 29 | * [Opera Driver](https://github.com/operasoftware/operachromiumdriver/releases) 30 | * Safari Driver ([Instructions](https://webkit.org/blog/6900/webdriver-support-in-safari-10/) to configure Safari to allow automation) 31 | 32 | ## Usage 33 | 34 | Simply run `linkedrw` to create your resume and personal webiste: 35 | 36 | This will create three outputs: 37 | 38 | `profile.json` - Your LinkedIn profile is being scraped and stored in this file 39 | 40 | `resume/` - The directory containing your resume files 41 | 42 | `website/` - The directory containing your personal website files 43 | 44 | ### Running Without LinkedIn 45 | 46 | Scraping from LinkedIn allows you to only manage and update your profile there 47 | while keeping your resume and personal website up-to-date. 48 | However, you can also create your resume and personal website by using a JSON file. 49 | Check out the example [here](example.json) for the JSON format that `linkedrw` accepts. 50 | Once you have your JSON profile ready, run the following command to create your resume and personal website: 51 | 52 | ```bash 53 | linkedrw -j example.json 54 | ``` 55 | 56 | ### Compiling Your Resume 57 | 58 | The `resume/` directory contains a list of LaTex files that can be compiled into a PDF resume file. 59 | As per the instructions and requirements from [Awesome-CV](https://github.com/posquit0/Awesome-CV), 60 | a full TeX distribution needs to be installed to compile the LaTex files. 61 | You can download and install it from [here](https://www.latex-project.org/get/#tex-distributions). 62 | 63 | Please note that `linkedrw` will try to compile the LaTex files for you if the requirements are met. 64 | 65 | After installing the TeX distribution, run the following commands to compile your resume: 66 | 67 | ```bash 68 | cd resume/ 69 | xelatex resume.tex 70 | ``` 71 | 72 | This should create your PDF resume file `resume.pdf` 73 | 74 | If your resume contains a publication section, 75 | [**BibLaTeX**](https://www.ctan.org/pkg/biblatex) and [**biber**](https://www.ctan.org/pkg/biber) should also be available. 76 | And run the following commands instead: 77 | 78 | ```bash 79 | cd resume/ 80 | xelatex resume.tex 81 | biber resume 82 | xelatex resume.tex 83 | ``` 84 | 85 | ### Personal Website 86 | 87 | Simply navigate to the `website/` directory and open `index.html` in a web browser, 88 | and you should be able to see your personal website. 89 | 90 | ### Options 91 | 92 | Below is the list of options: 93 | 94 | ```text 95 | -h, --help show this help message and exit 96 | --email EMAIL, -e EMAIL 97 | Your LinkedIn login email 98 | --password PASSWORD, -p PASSWORD 99 | Your LinkedIn login password 100 | --keep_creds, -k Store LinkedIn login credentials under 101 | ~/.linkedrw/credentials.json 102 | --output_dir OUTPUT_DIR, -o OUTPUT_DIR 103 | The output directory (default: current directory) 104 | --scrape_only, -s Only scrape LinkedIn profile 105 | --resume_only, -r Only create resume 106 | --website_only, -w Only create personal website 107 | --profile PROFILE_FILE, -j PROFILE_FILE 108 | The profile json file 109 | --driver DRIVER, -d DRIVER 110 | The web driver: chrome, firefox, safari, opera 111 | (default: chrome) 112 | --driver_path DRIVER_PATH, -dp DRIVER_PATH 113 | The executable path of the web driver 114 | --timeout TIMEOUT, -t TIMEOUT 115 | The timeout value (default: 10) 116 | ``` 117 | 118 | ## Customisation 119 | 120 | ### Customising Your Resume 121 | 122 | The comments in `resume.pdf` give you guidelines on customising your resume. 123 | 124 | ### Customising Your Personal Website 125 | 126 | Run the following commands to install the dependencies first: 127 | 128 | ```bash 129 | cd website/ 130 | npm install 131 | ``` 132 | 133 | Then run the following command so that it can be auto compiled when there are changes made to `js/scripts.js` or `sass/styles.css`: 134 | 135 | ```bash 136 | npm run watch 137 | ``` 138 | 139 | For more customisation instructions, please refer to the original [repo](https://github.com/RyanFitzgerald/devportfolio). 140 | 141 | ## Issues 142 | 143 | If `NoSuchElementException` is raised, try increasing the timeout value by specifying `-t/--timeout` option. 144 | If the problem remains, please raise an issue. 145 | -------------------------------------------------------------------------------- /linkedrw/scraper/personal.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from selenium.common.exceptions import NoSuchElementException 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support import expected_conditions as ec 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | 8 | from linkedrw.constants import * 9 | 10 | 11 | def get_personal_details(driver, section_type, timeout=None): 12 | """ 13 | Scrape personal details 14 | Args: 15 | driver: the selenium driver 16 | section_type: the section type 17 | timeout: the timeout value 18 | 19 | Returns: 20 | A list of details of all the items under the given section 21 | """ 22 | if section_type == NAME: 23 | return get_name(driver) 24 | elif section_type == POSITION: 25 | return get_position(driver) 26 | elif section_type == CONTACT: 27 | if timeout is None: 28 | raise ValueError("timeout needs to be provided") 29 | 30 | return get_contact(driver, timeout) 31 | elif section_type == SUMMARY: 32 | return get_summary(driver) 33 | 34 | 35 | def get_name(driver): 36 | """ 37 | Scrape name 38 | Args: 39 | driver: the selenium driver 40 | 41 | Returns: 42 | A string of name 43 | """ 44 | try: 45 | return driver.find_element_by_css_selector( 46 | ".pv-top-card-section__name.inline.t-24.t-black.t-normal" 47 | ).text 48 | except NoSuchElementException: 49 | return driver.find_element_by_css_selector( 50 | ".inline.t-24.t-black.t-normal.break-words" 51 | ).text 52 | 53 | 54 | def get_position(driver): 55 | """ 56 | Scrape position 57 | Args: 58 | driver: the selenium driver 59 | 60 | Returns: 61 | A string of the position 62 | """ 63 | try: 64 | position = driver.find_element_by_css_selector( 65 | ".pv-top-card-section__headline.mt1.t-18.t-black.t-normal" 66 | ).text 67 | except NoSuchElementException: 68 | position = driver.find_element_by_css_selector( 69 | ".mt1.t-18.t-black.t-normal" 70 | ).text 71 | 72 | return re.sub(r"\s+at.*", "", position) 73 | 74 | 75 | def get_contact(driver, timeout): 76 | """ 77 | Scrape contact details 78 | Args: 79 | driver: the selenium driver 80 | timeout: the timeout value 81 | 82 | Returns: 83 | A dict of contact details 84 | """ 85 | 86 | # Show contact details 87 | driver.find_element_by_xpath("//a[@data-control-name='contact_see_more']").click() 88 | 89 | linkedin_id = ( 90 | WebDriverWait(driver, timeout) 91 | .until( 92 | ec.presence_of_element_located( 93 | (By.CLASS_NAME, "pv-contact-info__ci-container") 94 | ) 95 | ) 96 | .find_element_by_tag_name("a") 97 | .get_attribute("href") 98 | ) 99 | email = ( 100 | driver.find_element_by_css_selector(".pv-contact-info__contact-type.ci-email") 101 | .find_element_by_tag_name("a") 102 | .text 103 | ) 104 | social_media = get_social_media(driver) 105 | 106 | try: 107 | mobile = ( 108 | driver.find_element_by_css_selector( 109 | ".pv-contact-info__contact-type.ci-phone" 110 | ) 111 | .find_element_by_css_selector(".t-14.t-black.t-normal") 112 | .text 113 | ) 114 | except NoSuchElementException: 115 | mobile = "" 116 | 117 | try: 118 | address = ( 119 | driver.find_element_by_css_selector( 120 | ".pv-contact-info__contact-type.ci-address" 121 | ) 122 | .find_element_by_tag_name("a") 123 | .text 124 | ) 125 | except NoSuchElementException: 126 | address = "" 127 | 128 | # Close contact details 129 | driver.find_element_by_xpath("//button[@aria-label='Dismiss']").click() 130 | 131 | results = { 132 | ADDRESS: address, 133 | MOBILE: mobile, 134 | EMAIL: email, 135 | HOMEPAGE: "", 136 | LINKEDIN: linkedin_id, 137 | SKYPE: "", 138 | } 139 | results.update(social_media) 140 | 141 | return results 142 | 143 | 144 | def get_social_media(driver): 145 | """ 146 | Get social media details 147 | Args: 148 | driver: the selenium driver 149 | 150 | Returns: 151 | A dict of social media details 152 | """ 153 | github = gitlab = stackoverflow = twitter = reddit = medium = scholar = "" 154 | try: 155 | websites_section = driver.find_element_by_css_selector( 156 | ".pv-contact-info__contact-type.ci-websites" 157 | ) 158 | for li in websites_section.find_elements_by_tag_name("li"): 159 | link = li.find_element_by_tag_name("a").get_attribute("href") 160 | if "github.com" in link: 161 | github = link 162 | elif "scholar.google.com" in link: 163 | scholar = link 164 | elif "gitlab.com" in link: 165 | gitlab = link 166 | elif "stackoverflow.com" in link: 167 | stackoverflow = link 168 | elif "twitter.com" in link: 169 | twitter = link 170 | elif "reddit" in link: 171 | reddit = link 172 | elif "medium.com" in link: 173 | medium = link 174 | except NoSuchElementException: 175 | pass 176 | 177 | results = { 178 | GITHUB: github, 179 | GITLAB: gitlab, 180 | STACKOVERFLOW: stackoverflow, 181 | TWITTER: twitter, 182 | REDDIT: reddit, 183 | MEDIUM: medium, 184 | GOOGLE_SCHOLAR: scholar, 185 | } 186 | 187 | return results 188 | 189 | 190 | def get_summary(driver): 191 | """ 192 | Scrape summary 193 | Args: 194 | driver: the selenium driver 195 | 196 | Returns: 197 | A string of summary 198 | """ 199 | # Check if summary section exists 200 | try: 201 | section = driver.find_element_by_css_selector( 202 | ".artdeco-container-card.pv-profile-section.pv-about-section.ember-view" 203 | ) 204 | except NoSuchElementException: 205 | return "" 206 | 207 | # Check if there is a show more button 208 | try: 209 | section.find_element_by_class_name("lt-line-clamp__more").click() 210 | except NoSuchElementException: 211 | pass 212 | 213 | try: 214 | return section.find_element_by_class_name("lt-line-clamp__raw-line").text 215 | except NoSuchElementException: 216 | return "" 217 | -------------------------------------------------------------------------------- /linkedrw/linkedr/resume.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkg_resources 3 | import shlex 4 | 5 | from logbook import Logger 6 | from subprocess import Popen, PIPE, TimeoutExpired 7 | from urllib.parse import urlparse 8 | 9 | from linkedrw.constants import * 10 | from linkedrw.utils import make_dir, copy_files 11 | from linkedrw.linkedr.publication import make_publication_section 12 | from linkedrw.linkedr.section import make_resume_section 13 | from linkedrw.linkedr.skill import make_skill_section 14 | 15 | 16 | def make_resume_files(profile, output_dir, timeout): 17 | """ 18 | Create resume files 19 | Args: 20 | profile: the dict of the profile 21 | output_dir: the output directory 22 | timeout: the timeout value 23 | 24 | Returns: 25 | None 26 | """ 27 | log = Logger() 28 | log.notice("Creating resume files") 29 | 30 | output_dir = os.path.join(output_dir, "resume") 31 | make_dir(output_dir) 32 | copy_files(__name__.split(".")[0], "templates/awesome_cv_files", output_dir) 33 | 34 | if PUBLICATIONS in profile: 35 | has_publications = make_publication_section(profile[PUBLICATIONS], output_dir) 36 | else: 37 | has_publications = False 38 | 39 | if SKILLS in profile and LANGUAGES in profile: 40 | make_skill_section(profile[SKILLS], profile[LANGUAGES], output_dir) 41 | 42 | for section in RESUME_SECTIONS: 43 | make_resume_section(profile, section, output_dir) 44 | 45 | make_resume_main(profile, has_publications, output_dir) 46 | compile_resume(output_dir, has_publications, timeout) 47 | 48 | 49 | def make_resume_main(profile, has_publications, output_dir): 50 | """ 51 | Create the main resume file 52 | Args: 53 | profile: the dict of the profile 54 | has_publications: the bool if there are publications 55 | output_dir: the output directory 56 | 57 | Returns: 58 | None 59 | """ 60 | lines = [] 61 | with open( 62 | pkg_resources.resource_filename(__name__.split(".")[0], RESUME_TEMPLATE) 63 | ) as f: 64 | for line in f: 65 | line = line.strip() 66 | if "personal-info-here" in line: 67 | lines += make_personal_info(profile) 68 | elif "resume-content-here" in line: 69 | lines += make_resume_content(profile) 70 | elif "addbibresource" in line and has_publications: 71 | lines.append(line.lstrip("% ")) 72 | else: 73 | if line.startswith("\\makecvfooter") and NAME in profile: 74 | line += f"{{\\today}}{{{profile[NAME]}~~~·~~~Resume}}{{\\thepage}}" 75 | 76 | lines.append(line) 77 | 78 | with open(os.path.join(output_dir, "resume.tex"), "w") as f: 79 | f.write("\n".join(lines)) 80 | 81 | 82 | def make_personal_info(profile): 83 | """ 84 | Create lines about the personal info 85 | Args: 86 | profile: the dict of the profile 87 | 88 | Returns: 89 | A list of lines about the personal info 90 | """ 91 | lines = [] 92 | for info_type in PERSONAL_INFO: 93 | if info_type in (NAME, POSITION) and info_type in profile: 94 | value = profile[info_type] 95 | elif CONTACT in profile and info_type in profile[CONTACT]: 96 | value = profile[CONTACT][info_type] 97 | else: 98 | value = "" 99 | 100 | line = f"\\{info_type}" 101 | if value: 102 | if info_type == NAME: 103 | names = value.split() 104 | last_name = " ".join(names[1:]) 105 | line += f"{{{names[0]}}}{{{last_name}}}" 106 | elif info_type == STACKOVERFLOW: 107 | user_id, username = ( 108 | urlparse(value).path.replace("/users/", "").strip("/").split("/") 109 | ) 110 | line += f"{{{user_id}}}{{{username}}}" 111 | elif info_type == GOOGLE_SCHOLAR: 112 | queries = urlparse(value).query.split("&") 113 | for query in queries: 114 | if "user=" in query: 115 | user_id = query.replace("user=", "").strip("/") 116 | line += f"{{{user_id}}}{{}}" 117 | 118 | break 119 | else: 120 | url_path = urlparse(value).path 121 | if info_type in (GITHUB, GITLAB): 122 | value = url_path.strip("/") 123 | elif info_type == LINKEDIN: 124 | value = url_path.lstrip("/in/").strip("/") 125 | elif info_type == TWITTER: 126 | user_id = url_path.strip("/") 127 | value = f"@{user_id}" 128 | elif info_type == REDDIT: 129 | value = url_path.replace("/user/", "").strip("/") 130 | elif info_type == MEDIUM: 131 | value = url_path.lstrip("/@").strip("/") 132 | 133 | line += f"{{{value}}}" 134 | else: 135 | line = f"% {line}{{}}" 136 | 137 | lines.append(line) 138 | 139 | return lines 140 | 141 | 142 | def make_resume_content(profile): 143 | """ 144 | Create lines about additional section 145 | Args: 146 | profile: the dict of the profile 147 | 148 | Returns: 149 | A list of lines about additional section 150 | """ 151 | lines = [] 152 | for section in RESUME_CONTENT: 153 | if section in profile and profile[section]: 154 | lines.append(f"\\input{{{section}.tex}}") 155 | 156 | return lines 157 | 158 | 159 | def compile_resume(output_dir, has_pubs, timeout): 160 | """ 161 | Compile resume files 162 | Args: 163 | output_dir: the resume output directory 164 | has_pubs: the boolean whether there is a publication section 165 | timeout: the timeout value 166 | 167 | Returns: 168 | None 169 | """ 170 | log = Logger() 171 | log.notice("Compiling resume files") 172 | curr_dir = os.getcwd() 173 | os.chdir(output_dir) 174 | 175 | if run_cmd("xelatex resume.tex", timeout): 176 | if has_pubs and ( 177 | not run_cmd("biber resume", timeout) 178 | or not run_cmd("xelatex resume.tex", timeout) 179 | ): 180 | log.warn("Failed to compile resume files, please compile them manually") 181 | else: 182 | log.warn("Failed to compile resume files, please compile them manually") 183 | 184 | os.chdir(curr_dir) 185 | 186 | 187 | def run_cmd(cmd, timeout): 188 | """ 189 | Run a shell command 190 | Args: 191 | cmd: the string of command to run 192 | timeout: the timeout value 193 | 194 | Returns: 195 | A boolean whether the command has been successful or not 196 | """ 197 | success = True 198 | try: 199 | proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE) 200 | _, err = proc.communicate(timeout=timeout) 201 | 202 | if proc.returncode != 0 or err: 203 | success = False 204 | except (FileNotFoundError, TimeoutExpired): 205 | success = False 206 | 207 | return success 208 | -------------------------------------------------------------------------------- /tests/website/full/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | My Portfolio 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | Close 23 |
24 | 44 |
45 | 46 | 47 |
48 |
49 |

Name

50 |

Position

51 | Download Resume 52 |
53 | 54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 |
67 |
68 |
69 |
70 |

About Me

71 |
72 |
73 |

74 | About 75 |

76 |
77 |
78 |
79 |
80 | 81 | 82 |
83 |

Experience

84 |
85 |
86 |

Company

87 |

Title

88 |
    89 |
  • Description 90 |
91 |
92 | 93 |
94 |
95 | 96 | 97 |
98 |

Education

99 |
100 |

School

101 | 2018 - 2019 102 |

Degree

103 |
    104 |
  • Description 105 |
106 |
107 | 108 |
109 | 110 | 111 |
112 |

Projects

113 |
114 |
115 |
116 |
117 |
118 |

Project

119 |
    120 |
  • Description 121 |
122 | View Project 123 |
124 |
125 |
126 | 127 |
128 |
129 |
130 | 131 | 132 |
133 |

Skills

134 |
    135 |
  • Python
  • 136 |
  • Java
  • 137 |
  • Docker
  • 138 |
139 |
140 | 141 | 142 |
143 |

Get in Touch

144 |
145 |
146 | 147 | 148 | 149 | 150 |
151 |
152 | 153 |
154 | 155 | 156 |
157 |
158 |
159 | 164 |
165 | 166 | 167 | 168 |
169 | 191 | 192 |
193 |
194 |
195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /linkedrw/utils/prog_languages.txt: -------------------------------------------------------------------------------- 1 | a# .net 2 | a-0 system 3 | a+ 4 | a++ 5 | abap 6 | abc 7 | abc algol 8 | abset 9 | absys 10 | acc 11 | accent 12 | ace dasl 13 | acl2 14 | act-iii 15 | action! 16 | actionscript 17 | actor 18 | ada 19 | adenine 20 | agda 21 | agilent vee 22 | agora 23 | aimms 24 | aldor 25 | alef 26 | alf 27 | algol 58 28 | algol 60 29 | algol 68 30 | algol w 31 | alice 32 | alma-0 33 | ambienttalk 34 | amiga e 35 | amos 36 | ampl 37 | angelscript 38 | apex 39 | apl 40 | app inventor for android's visual block language 41 | applescript 42 | apt 43 | arc 44 | arexx 45 | argus 46 | aspectj 47 | assembly language 48 | ats 49 | ateji px 50 | autohotkey 51 | autocoder 52 | autoit 53 | autolisp / visual lisp 54 | averest 55 | awk 56 | axum 57 | active server pages 58 | b 59 | babbage 60 | ballerina 61 | bash 62 | basic 63 | bc 64 | bcpl 65 | beanshell 66 | batch file 67 | bertrand 68 | beta 69 | bliss 70 | blockly 71 | bloop 72 | boo 73 | boomerang 74 | bourne shell 75 | bpel 76 | business basic 77 | c 78 | c-- 79 | c++ 80 | c* 81 | c# 82 | c/al 83 | caché objectscript 84 | c shell 85 | caml 86 | cayenne 87 | cduce 88 | cecil 89 | cesil 90 | céu 91 | ceylon 92 | cfengine 93 | cg 94 | ch 95 | chapel 96 | charity 97 | charm 98 | chill 99 | chip-8 100 | chomski 101 | chuck 102 | cilk 103 | citrine 104 | cl 105 | claire 106 | clarion 107 | clean 108 | clipper 109 | clips 110 | clist 111 | clojure 112 | clu 113 | cms-2 114 | cobol 115 | cobolscript 116 | cobra 117 | coffeescript 118 | coldfusion 119 | comal 120 | combined programming language 121 | comit 122 | common intermediate language 123 | common lisp 124 | compass 125 | component pascal 126 | constraint handling rules 127 | comtran 128 | cool 129 | coq 130 | coral 66 131 | corvision 132 | cowsel 133 | cpl 134 | cryptol 135 | crystal 136 | csound 137 | csp 138 | cuneiform 139 | curl 140 | curry 141 | cybil 142 | cyclone 143 | cython 144 | css 145 | d 146 | dasl 147 | dart 148 | darwin 149 | dataflex 150 | datalog 151 | datatrieve 152 | dbase 153 | dc 154 | dcl 155 | dinkc 156 | dibol 157 | dog 158 | draco 159 | dragon 160 | drakon 161 | dylan 162 | dynamo 163 | dax 164 | e 165 | ease 166 | easy pl/i 167 | easytrieve plus 168 | ec 169 | ecmascript 170 | edinburgh imp 171 | egl 172 | eiffel 173 | elan 174 | elixir 175 | elm 176 | emacs lisp 177 | emerald 178 | epigram 179 | epl 180 | epl 181 | erlang 182 | es 183 | escher 184 | espol 185 | esterel 186 | etoys 187 | euclid 188 | euler 189 | euphoria 190 | euslisp robot programming language 191 | cms exec 192 | exec 2 193 | executable uml 194 | ezhil 195 | f 196 | f# 197 | f* 198 | factor 199 | fantom 200 | faust 201 | ffp 202 | fjölnir 203 | fl 204 | flavors 205 | flex 206 | floop 207 | flow-matic 208 | focal 209 | focus 210 | foil 211 | formac 212 | @formula 213 | forth 214 | fortran 215 | fortress 216 | foxpro 217 | fp 218 | franz lisp 219 | f-script 220 | flutter 221 | game maker language 222 | gamemonkey script 223 | gams 224 | gap 225 | g-code 226 | gdscript 227 | genie 228 | gdl 229 | george 230 | glsl 231 | gnu e 232 | go 233 | go! 234 | goal 235 | gödel 236 | golo 237 | gom 238 | google apps script 239 | gosu 240 | gotran 241 | gpss 242 | graphtalk 243 | grass 244 | grasshopper 245 | groovy 246 | hack 247 | haggis 248 | hal/s 249 | halide 250 | hamilton c shell 251 | harbour 252 | hartmann pipelines 253 | haskell 254 | haxe 255 | hermes 256 | high level assembly 257 | hlsl 258 | holyc 259 | hop 260 | hopscotch 261 | hope 262 | hugo 263 | hume 264 | hypertalk 265 | io 266 | icon 267 | ibm basic assembly language 268 | ibm basica 269 | ibm hascript 270 | ibm informix-4gl 271 | ibm rpg 272 | irineu 273 | idl 274 | idris 275 | inform 276 | j 277 | j# 278 | j++ 279 | jade 280 | jal 281 | janus 282 | janus 283 | jass 284 | java 285 | javafx script 286 | javascript 287 | jcl 288 | jean 289 | join java 290 | joss 291 | joule 292 | jovial 293 | joy 294 | jscript 295 | jscript .net 296 | json 297 | julia 298 | jython 299 | k 300 | kaleidoscope 301 | karel 302 | kee 303 | kixtart 304 | klerer-may system 305 | kif 306 | kojo 307 | kotlin 308 | krc 309 | krl 310 | krl 311 | krypton 312 | korn shell 313 | kodu 314 | kv 315 | labview 316 | ladder 317 | lansa 318 | lasso 319 | latex 320 | lava 321 | lc-3 322 | legoscript 323 | lil 324 | lilypond 325 | limbo 326 | limnor 327 | linc 328 | lingo 329 | linq 330 | lis 331 | lisa 332 | lisp 333 | lite-c 334 | lithe 335 | little b 336 | lll 337 | logo 338 | logtalk 339 | lotusscript 340 | lpc 341 | lse 342 | lsl 343 | livecode 344 | livescript 345 | lua 346 | lucid 347 | lustre 348 | lyapas 349 | lynx 350 | m 351 | m2001 352 | m4 353 | m# 354 | machine code 355 | mad 356 | mad/i 357 | magik 358 | magma 359 | make 360 | maude system 361 | maple 362 | mapper 363 | mark-iv 364 | mary 365 | masm microsoft assembly x86 366 | math-matic 367 | mathematica 368 | matlab 369 | maxima 370 | max 371 | maxscript internal language 3d studio max 372 | maya 373 | mdl 374 | mercury 375 | mesa 376 | metafont 377 | metaquotes language 378 | mheg-5 379 | microcode 380 | microscript 381 | miis 382 | milk 383 | mimic 384 | mirah 385 | miranda 386 | miva script 387 | ml 388 | model 204 389 | modelica 390 | modula 391 | modula-2 392 | modula-3 393 | mohol 394 | moo 395 | mortran 396 | mouse 397 | mpd 398 | mathcad 399 | msil 400 | msl 401 | mumps 402 | mupad 403 | mutan 404 | mystic programming language 405 | nasm 406 | napier88 407 | neko 408 | nemerle 409 | nesl 410 | net.data 411 | netlogo 412 | netrexx 413 | newlisp 414 | newp 415 | newspeak 416 | newtonscript 417 | nial 418 | nice 419 | nickle 420 | nim 421 | npl 422 | not exactly c 423 | not quite c 424 | nsis 425 | nu 426 | nwscript 427 | nxt-g 428 | o:xml 429 | oak 430 | oberon 431 | obj2 432 | object lisp 433 | objectlogo 434 | object rexx 435 | object pascal 436 | objective-c 437 | objective-j 438 | obliq 439 | ocaml 440 | occam 441 | occam-π 442 | octave 443 | omnimark 444 | onyx 445 | opa 446 | opal 447 | opencl 448 | openedge abl 449 | opl 450 | openvera 451 | ops5 452 | optimj 453 | orc 454 | orca/modula-2 455 | oriel 456 | orwell 457 | oxygene 458 | oz 459 | p 460 | p4 461 | p′′ 462 | parasail 463 | pari/gp 464 | pascal 465 | pcastl 466 | pcf 467 | pearl 468 | peoplecode 469 | perl 470 | pdl 471 | perl 6 472 | pharo 473 | php 474 | pico 475 | picolisp 476 | pict 477 | pig 478 | pike 479 | pikt 480 | pilot 481 | pipelines 482 | pizza 483 | pl-11 484 | pl/0 485 | pl/b 486 | pl/c 487 | pl/i 488 | pl/m 489 | pl/p 490 | pl/sql 491 | pl360 492 | planc 493 | plankalkül 494 | planner 495 | plex 496 | plexil 497 | plus 498 | pop-11 499 | pop-2 500 | postscript 501 | portable 502 | pov-ray sdl 503 | powerhouse 504 | powerbuilder 505 | powershell 506 | ppl 507 | processing 508 | processing.js 509 | prograph 510 | proiv 511 | prolog 512 | promal 513 | promela 514 | prose modeling language 515 | protel 516 | providex 517 | pro*c 518 | pure 519 | purebasic 520 | pure data 521 | python 522 | q 523 | q# 524 | qalb 525 | qtscript 526 | quakec 527 | qpl 528 | r 529 | r++ 530 | racket 531 | rapid 532 | rapira 533 | ratfiv 534 | ratfor 535 | rc 536 | reason 537 | rebol 538 | red 539 | redcode 540 | refal 541 | rexx 542 | ring 543 | rlab 544 | roop 545 | rpg 546 | rpl 547 | rsl 548 | rtl/2 549 | ruby 550 | runescript 551 | rust 552 | s 553 | s2 554 | s3 555 | s-lang 556 | s-plus 557 | sa-c 558 | sabretalk 559 | sail 560 | salsa 561 | sam76 562 | sas 563 | sasl 564 | sather 565 | sawzall 566 | scala 567 | scheme 568 | scilab 569 | scratch 570 | script.net 571 | sed 572 | seed7 573 | self 574 | sensetalk 575 | sequencel 576 | serpent 577 | setl 578 | simpol 579 | signal 580 | simple 581 | simscript 582 | simula 583 | simulink 584 | singularity 585 | sisal 586 | slip 587 | small 588 | smalltalk 589 | sml 590 | strongtalk 591 | snap! 592 | snobol 593 | snowball 594 | sol 595 | solidity 596 | sophaeros 597 | spark 598 | speakeasy 599 | speedcode 600 | spin 601 | sp/k 602 | sps 603 | sql 604 | sqr 605 | squeak 606 | squirrel 607 | sr 608 | s/sl 609 | starlogo 610 | strand 611 | stata 612 | stateflow 613 | subtext 614 | sbl 615 | supercollider 616 | supertalk 617 | swift 618 | swift 619 | sympl 620 | systemverilog 621 | t 622 | tacl 623 | tacpol 624 | tads 625 | tal 626 | tcl 627 | tea 628 | teco 629 | telcomp 630 | tex 631 | tex 632 | tie 633 | tmg, compiler-compiler 634 | tom 635 | tom 636 | toi 637 | topspeed 638 | tpu 639 | trac 640 | ttm 641 | t-sql 642 | transcript 643 | ttcn 644 | turing 645 | tutor 646 | txl 647 | typescript 648 | tynker 649 | ubercode 650 | ucsd pascal 651 | umple 652 | unicon 653 | uniface 654 | unity 655 | unix shell 656 | unrealscript 657 | vala 658 | verilog 659 | vhdl 660 | vim script 661 | viper 662 | visual basic 663 | visual basic .net 664 | visual dataflex 665 | visual dialogscript 666 | visual fortran 667 | visual foxpro 668 | visual j++ 669 | visual j# 670 | visual lisp 671 | visual objects 672 | visual prolog 673 | vsxu 674 | watfiv, watfor 675 | webassembly 676 | webdna 677 | whiley 678 | winbatch 679 | wolfram language 680 | wyvern 681 | x++ 682 | x10 683 | xbase 684 | xbase++ 685 | xbl 686 | xc 687 | xharbour 688 | xl 689 | xojo 690 | xotcl 691 | xod 692 | xpath 693 | xpl 694 | xpl0 695 | xquery 696 | xsb 697 | xsharp 698 | xslt 699 | xtend 700 | yorick 701 | yql 702 | yoix 703 | z notation 704 | zebra, zpl, zpl2 705 | zeno 706 | zetalisp 707 | zopl 708 | zsh 709 | zpl 710 | z++ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/latex,macos,python,pycharm 3 | # Edit at https://www.gitignore.io/?templates=latex,macos,python,pycharm 4 | 5 | ### LaTeX ### 6 | ## Core latex/pdflatex auxiliary files: 7 | *.aux 8 | *.lof 9 | *.log 10 | *.lot 11 | *.fls 12 | *.out 13 | *.toc 14 | *.fmt 15 | *.fot 16 | *.cb 17 | *.cb2 18 | .*.lb 19 | 20 | ## Intermediate documents: 21 | *.dvi 22 | *.xdv 23 | *-converted-to.* 24 | # these rules might exclude image files for figures etc. 25 | # *.ps 26 | # *.eps 27 | # *.pdf 28 | 29 | ## Generated if empty string is given at "Please type another file name for output:" 30 | .pdf 31 | 32 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 33 | *.bbl 34 | *.bcf 35 | *.blg 36 | *-blx.aux 37 | *-blx.bib 38 | *.run.xml 39 | 40 | ## Build tool auxiliary files: 41 | *.fdb_latexmk 42 | *.synctex 43 | *.synctex(busy) 44 | *.synctex.gz 45 | *.synctex.gz(busy) 46 | *.pdfsync 47 | 48 | ## Build tool directories for auxiliary files 49 | # latexrun 50 | latex.out/ 51 | 52 | ## Auxiliary and intermediate files from other packages: 53 | # algorithms 54 | *.alg 55 | *.loa 56 | 57 | # achemso 58 | acs-*.bib 59 | 60 | # amsthm 61 | *.thm 62 | 63 | # beamer 64 | *.nav 65 | *.pre 66 | *.snm 67 | *.vrb 68 | 69 | # changes 70 | *.soc 71 | 72 | # comment 73 | *.cut 74 | 75 | # cprotect 76 | *.cpt 77 | 78 | # elsarticle (documentclass of Elsevier journals) 79 | *.spl 80 | 81 | # endnotes 82 | *.ent 83 | 84 | # fixme 85 | *.lox 86 | 87 | # feynmf/feynmp 88 | *.mf 89 | *.mp 90 | *.t[1-9] 91 | *.t[1-9][0-9] 92 | *.tfm 93 | 94 | #(r)(e)ledmac/(r)(e)ledpar 95 | *.end 96 | *.?end 97 | *.[1-9] 98 | *.[1-9][0-9] 99 | *.[1-9][0-9][0-9] 100 | *.[1-9]R 101 | *.[1-9][0-9]R 102 | *.[1-9][0-9][0-9]R 103 | *.eledsec[1-9] 104 | *.eledsec[1-9]R 105 | *.eledsec[1-9][0-9] 106 | *.eledsec[1-9][0-9]R 107 | *.eledsec[1-9][0-9][0-9] 108 | *.eledsec[1-9][0-9][0-9]R 109 | 110 | # glossaries 111 | *.acn 112 | *.acr 113 | *.glg 114 | *.glo 115 | *.gls 116 | *.glsdefs 117 | 118 | # gnuplottex 119 | *-gnuplottex-* 120 | 121 | # gregoriotex 122 | *.gaux 123 | *.gtex 124 | 125 | # htlatex 126 | *.4ct 127 | *.4tc 128 | *.idv 129 | *.lg 130 | *.trc 131 | *.xref 132 | 133 | # hyperref 134 | *.brf 135 | 136 | # knitr 137 | *-concordance.tex 138 | # TODO Comment the next line if you want to keep your tikz graphics files 139 | *.tikz 140 | *-tikzDictionary 141 | 142 | # listings 143 | *.lol 144 | 145 | # luatexja-ruby 146 | *.ltjruby 147 | 148 | # makeidx 149 | *.idx 150 | *.ilg 151 | *.ind 152 | *.ist 153 | 154 | # minitoc 155 | *.maf 156 | *.mlf 157 | *.mlt 158 | *.mtc[0-9]* 159 | *.slf[0-9]* 160 | *.slt[0-9]* 161 | *.stc[0-9]* 162 | 163 | # minted 164 | _minted* 165 | *.pyg 166 | 167 | # morewrites 168 | *.mw 169 | 170 | # nomencl 171 | *.nlg 172 | *.nlo 173 | *.nls 174 | 175 | # pax 176 | *.pax 177 | 178 | # pdfpcnotes 179 | *.pdfpc 180 | 181 | # sagetex 182 | *.sagetex.sage 183 | *.sagetex.py 184 | *.sagetex.scmd 185 | 186 | # scrwfile 187 | *.wrt 188 | 189 | # sympy 190 | *.sout 191 | *.sympy 192 | sympy-plots-for-*.tex/ 193 | 194 | # pdfcomment 195 | *.upa 196 | *.upb 197 | 198 | # pythontex 199 | *.pytxcode 200 | pythontex-files-*/ 201 | 202 | # tcolorbox 203 | *.listing 204 | 205 | # thmtools 206 | *.loe 207 | 208 | # TikZ & PGF 209 | *.dpth 210 | *.md5 211 | *.auxlock 212 | 213 | # todonotes 214 | *.tdo 215 | 216 | # vhistory 217 | *.hst 218 | *.ver 219 | 220 | # easy-todo 221 | *.lod 222 | 223 | # xcolor 224 | *.xcp 225 | 226 | # xmpincl 227 | *.xmpi 228 | 229 | # xindy 230 | *.xdy 231 | 232 | # xypic precompiled matrices 233 | *.xyc 234 | 235 | # endfloat 236 | *.ttt 237 | *.fff 238 | 239 | # Latexian 240 | TSWLatexianTemp* 241 | 242 | ## Editors: 243 | # WinEdt 244 | *.bak 245 | *.sav 246 | 247 | # Texpad 248 | .texpadtmp 249 | 250 | # LyX 251 | *.lyx~ 252 | 253 | # Kile 254 | *.backup 255 | 256 | # KBibTeX 257 | *~[0-9]* 258 | 259 | # auto folder when using emacs and auctex 260 | ./auto/* 261 | *.el 262 | 263 | # expex forward references with \gathertags 264 | *-tags.tex 265 | 266 | # standalone packages 267 | *.sta 268 | 269 | ### LaTeX Patch ### 270 | # glossaries 271 | *.glstex 272 | 273 | ### macOS ### 274 | # General 275 | .DS_Store 276 | .AppleDouble 277 | .LSOverride 278 | 279 | # Icon must end with two \r 280 | Icon 281 | 282 | # Thumbnails 283 | ._* 284 | 285 | # Files that might appear in the root of a volume 286 | .DocumentRevisions-V100 287 | .fseventsd 288 | .Spotlight-V100 289 | .TemporaryItems 290 | .Trashes 291 | .VolumeIcon.icns 292 | .com.apple.timemachine.donotpresent 293 | 294 | # Directories potentially created on remote AFP share 295 | .AppleDB 296 | .AppleDesktop 297 | Network Trash Folder 298 | Temporary Items 299 | .apdisk 300 | 301 | ### PyCharm ### 302 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 303 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 304 | 305 | # User-specific stuff 306 | .idea/**/workspace.xml 307 | .idea/**/tasks.xml 308 | .idea/**/usage.statistics.xml 309 | .idea/**/dictionaries 310 | .idea/**/shelf 311 | 312 | # Generated files 313 | .idea/**/contentModel.xml 314 | 315 | # Sensitive or high-churn files 316 | .idea/**/dataSources/ 317 | .idea/**/dataSources.ids 318 | .idea/**/dataSources.local.xml 319 | .idea/**/sqlDataSources.xml 320 | .idea/**/dynamic.xml 321 | .idea/**/uiDesigner.xml 322 | .idea/**/dbnavigator.xml 323 | 324 | # Gradle 325 | .idea/**/gradle.xml 326 | .idea/**/libraries 327 | 328 | # Gradle and Maven with auto-import 329 | # When using Gradle or Maven with auto-import, you should exclude module files, 330 | # since they will be recreated, and may cause churn. Uncomment if using 331 | # auto-import. 332 | # .idea/modules.xml 333 | # .idea/*.iml 334 | # .idea/modules 335 | 336 | # CMake 337 | cmake-build-*/ 338 | 339 | # Mongo Explorer plugin 340 | .idea/**/mongoSettings.xml 341 | 342 | # File-based project format 343 | *.iws 344 | 345 | # IntelliJ 346 | out/ 347 | 348 | # mpeltonen/sbt-idea plugin 349 | .idea_modules/ 350 | 351 | # JIRA plugin 352 | atlassian-ide-plugin.xml 353 | 354 | # Cursive Clojure plugin 355 | .idea/replstate.xml 356 | 357 | # Crashlytics plugin (for Android Studio and IntelliJ) 358 | com_crashlytics_export_strings.xml 359 | crashlytics.properties 360 | crashlytics-build.properties 361 | fabric.properties 362 | 363 | # Editor-based Rest Client 364 | .idea/httpRequests 365 | 366 | # Android studio 3.1+ serialized cache file 367 | .idea/caches/build_file_checksums.ser 368 | 369 | # JetBrains templates 370 | **___jb_tmp___ 371 | 372 | ### PyCharm Patch ### 373 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 374 | 375 | # *.iml 376 | # modules.xml 377 | # .idea/misc.xml 378 | # *.ipr 379 | 380 | # Sonarlint plugin 381 | .idea/sonarlint 382 | 383 | ### Python ### 384 | # Byte-compiled / optimized / DLL files 385 | __pycache__/ 386 | *.py[cod] 387 | *$py.class 388 | 389 | # C extensions 390 | *.so 391 | 392 | # Distribution / packaging 393 | .Python 394 | build/ 395 | develop-eggs/ 396 | dist/ 397 | downloads/ 398 | eggs/ 399 | .eggs/ 400 | lib/ 401 | lib64/ 402 | parts/ 403 | sdist/ 404 | var/ 405 | wheels/ 406 | pip-wheel-metadata/ 407 | share/python-wheels/ 408 | *.egg-info/ 409 | .installed.cfg 410 | *.egg 411 | MANIFEST 412 | 413 | # PyInstaller 414 | # Usually these files are written by a python script from a template 415 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 416 | *.manifest 417 | *.spec 418 | 419 | # Installer logs 420 | pip-log.txt 421 | pip-delete-this-directory.txt 422 | 423 | # Unit test / coverage reports 424 | htmlcov/ 425 | .tox/ 426 | .nox/ 427 | .coverage 428 | .coverage.* 429 | .cache 430 | nosetests.xml 431 | coverage.xml 432 | *.cover 433 | .hypothesis/ 434 | .pytest_cache/ 435 | 436 | # Translations 437 | *.mo 438 | *.pot 439 | 440 | # Django stuff: 441 | local_settings.py 442 | db.sqlite3 443 | 444 | # Flask stuff: 445 | instance/ 446 | .webassets-cache 447 | 448 | # Scrapy stuff: 449 | .scrapy 450 | 451 | # Sphinx documentation 452 | docs/_build/ 453 | 454 | # PyBuilder 455 | target/ 456 | 457 | # Jupyter Notebook 458 | .ipynb_checkpoints 459 | 460 | # IPython 461 | profile_default/ 462 | ipython_config.py 463 | 464 | # pyenv 465 | .python-version 466 | 467 | # pipenv 468 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 469 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 470 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 471 | # install all needed dependencies. 472 | #Pipfile.lock 473 | 474 | # celery beat schedule file 475 | celerybeat-schedule 476 | 477 | # SageMath parsed files 478 | *.sage.py 479 | 480 | # Environments 481 | .env 482 | .venv 483 | env/ 484 | venv/ 485 | ENV/ 486 | env.bak/ 487 | venv.bak/ 488 | 489 | # Spyder project settings 490 | .spyderproject 491 | .spyproject 492 | 493 | # Rope project settings 494 | .ropeproject 495 | 496 | # mkdocs documentation 497 | /site 498 | 499 | # mypy 500 | .mypy_cache/ 501 | .dmypy.json 502 | dmypy.json 503 | 504 | # Pyre type checker 505 | .pyre/ 506 | 507 | # End of https://www.gitignore.io/api/latex,macos,python,pycharm -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/css/styles.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Title: Dev Portfolio Template 3 | Version: 1.2.1 4 | Last Change: 08/28/2017 5 | Author: Ryan Fitzgerald 6 | Repo: https://github.com/RyanFitzgerald/devportfolio-template 7 | Issues: https://github.com/RyanFitzgerald/devportfolio-template/issues 8 | 9 | Description: This file contains all the styles associated with the page 10 | that don't come from third party libraries. This file gets compiled using 11 | Gulp and send to the /css folder which is then loaded on the page. 12 | */body{font-family:'Lato', sans-serif;font-size:16px}body.active{overflow:hidden;z-index:-1}.no-js #experience-timeline>div{background:#fff;padding:10px;margin-bottom:10px;border:1px solid #dcd9d9}.no-js #experience-timeline>div h3{font-size:1.5em;font-weight:300;color:#374054;display:inline-block;margin:0}.no-js #experience-timeline>div h4{font-size:1.2em;font-weight:300;color:#7e8890;margin:0 0 15px 0}.no-js #experience-timeline>div p{color:#74808a;font-size:0.9em;margin:0}.no-js #experience-timeline:before,.no-js #experience-timeline:after{content:none}@keyframes dropHeader{0%{transform:translateY(-100%)}100%{transform:translateY(0)}}header{position:absolute;top:0;left:0;right:0;text-align:center;z-index:10;animation-name:dropHeader;animation-iteration-count:1;animation-timing-function:ease;animation-duration:0.75s}header ul{display:inline-block;background:#fff;text-align:center;padding:10px;margin:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}header li{display:inline-block}header a{display:block;color:#3498db;padding:10px}header a:hover{color:#217dbb;text-decoration:none;background:#eee;border-radius:4px}header a:focus{color:#3498db;text-decoration:none}header.active{display:block}header.sticky{position:fixed;z-index:999}#menu.active{display:block}#mobile-menu-open{display:none;cursor:pointer;position:fixed;right:15px;top:10px;color:#3498db;font-size:1.5em;z-index:20;padding:0 7px;border-radius:4px;background:#fff}#mobile-menu-close{display:none;text-align:right;width:100%;background:#fff;font-size:1.5em;padding-right:15px;padding-top:10px;cursor:pointer;color:#3498db}#mobile-menu-close span{font-size:0.5em;text-transform:uppercase}#mobile-menu-close i{vertical-align:middle}footer{padding:50px 0}.copyright{padding-top:20px}.copyright p{margin:0;color:#74808a}.top{text-align:center}.top span{cursor:pointer;display:block;margin:15px auto 0 auto;width:35px;height:35px;border-radius:50%;border:3px solid #b9bfc4;text-align:center}.top i{color:#74808a}.social{text-align:right}.social ul{margin:5px 0 0 0;padding:0}.social li{display:inline-block;font-size:1.25em;list-style:none}.social a{display:block;color:#74808a;padding:10px}.social a:hover{color:#3498db}.btn-rounded-white{display:inline-block;color:#fff;padding:15px 25px;border:3px solid #fff;border-radius:30px;transition:.5s ease all}.btn-rounded-white:hover{color:#3498db;background:#fff;text-decoration:none}.shadow{box-shadow:0 1px 3px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24)}.shadow-large{box-shadow:0 3px 6px rgba(0,0,0,0.08),0 3px 6px rgba(0,0,0,0.15)}.heading{position:relative;display:inline-block;font-size:2em;font-weight:300;margin:0 0 30px 0}.heading:after{position:absolute;content:'';top:100%;height:1px;width:50px;left:0;right:0;margin:0 auto;background:#3498db}.background-alt{background:#f2f2f5}#lead{position:relative;height:100vh;min-height:500px;max-height:1080px;background:url(../images/lead-bg.jpg);background-size:cover;padding:15px;overflow:hidden}#lead-content{position:absolute;z-index:10;top:50%;left:50%;transform:translate(-50%, -50%);text-align:center}#lead-content h1,#lead-content h2{margin:0}#lead-content h1{color:#fff;font-weight:900;font-size:5em;text-transform:uppercase;letter-spacing:0.05em;line-height:0.9em}#lead-content h2{color:#a0cfee;font-weight:500;font-size:2.25em;margin-bottom:15px}#lead-overlay{position:absolute;height:100%;width:100%;top:0;right:0;bottom:0;left:0;background:rgba(33,125,187,0.8);z-index:1}#lead-down{position:absolute;left:0;right:0;width:100%;text-align:center;z-index:10;bottom:15px;color:#fff}#lead-down span{cursor:pointer;display:block;margin:0 auto;width:35px;height:35px;border-radius:50%;border:3px solid #a0cfee;text-align:center}#lead-down i{animation:pulsate 1.5s ease;animation-iteration-count:infinite;padding-top:5px}@keyframes pulsate{0%{transform:scale(1, 1)}50%{transform:scale(1.2, 1.2)}100%{transform:scale(1, 1)}}#about{padding:75px 15px;border-bottom:1px solid #dcd9d9}#about h2{color:#374054}#about p{color:#74808a;margin:0}#experience{padding:50px 15px;text-align:center;border-bottom:1px solid #dcd9d9}#experience h2{color:#374054}#experience-timeline{margin:30px auto 0 auto;position:relative;max-width:1000px}#experience-timeline:before{position:absolute;content:'';top:0;bottom:0;left:303px;right:auto;height:100%;width:3px;background:#3498db;z-index:0}#experience-timeline:after{position:absolute;content:'';width:3px;height:40px;background:#3498db;background:linear-gradient(to bottom, #3498db, rgba(52,152,219,0));top:100%;left:303px}.vtimeline-content{margin-left:350px;background:#fff;border:1px solid #e6e6e6;padding:15px;border-radius:3px;text-align:left}.vtimeline-content h3{font-size:1.5em;font-weight:300;color:#374054;display:inline-block;margin:0}.vtimeline-content h4{font-size:1.2em;font-weight:300;color:#7e8890;margin:0 0 15px 0}.vtimeline-content p,.vtimeline-content ul{color:#74808a;font-size:0.9em;margin:0}.vtimeline-content ul{padding:0 0 0 15px}.vtimeline-point{position:relative;display:block;vertical-align:top;margin-bottom:30px}.vtimeline-icon{position:relative;color:#fff;width:50px;height:50px;background:#3498db;border-radius:50%;float:left;z-index:99;margin-left:280px}.vtimeline-icon i{display:block;font-size:2em;margin-top:10px}.vtimeline-date{width:260px;text-align:right;position:absolute;left:0;top:10px;font-weight:300;color:#374054}#education{padding:50px 15px 20px 15px;border-bottom:1px solid #dcd9d9;text-align:center}#education h2{color:#374054;margin-bottom:50px}.education-block{max-width:700px;margin:0 auto 30px auto;padding:15px;border:1px solid #dcd9d9;text-align:left}.education-block h3{font-weight:500;float:left;margin:0;color:#374054}.education-block span{color:#74808a;float:right}.education-block h4{color:#74808a;clear:both;font-weight:500;margin:0 0 15px 0}.education-block p,.education-block ul{margin:0;color:#74808a;font-size:0.9em}.education-block ul{padding:0 0 0 15px}#projects{padding:50px 15px;border-bottom:1px solid #dcd9d9;text-align:center}#projects h2{color:#374054;margin-bottom:50px}.project{position:relative;max-width:900px;margin:0 auto 30px auto;overflow:hidden;background:#fff;border-radius:4px}.project-image{float:left}.project-info{position:absolute;top:50%;transform:translateY(-50%);margin-left:300px;padding:15px}.project-info h3{font-size:1.5em;font-weight:300;color:#374054;margin:0 0 15px 0}.project-info p,.project-info ul{color:#74808a;margin:0 0 15px 0;font-size:0.9em;text-align:left}.no-image .project-info{position:relative;margin:0;padding:30px 15px;transform:none}#more-projects{display:none}#skills{padding:50px 15px;text-align:center}#skills h2{color:#374054;margin-bottom:50px}#skills ul{display:block;margin:0 auto;padding:0;max-width:800px}#skills li{display:inline-block;margin:7px;padding:5px 10px;color:#374054;background:#e4e4ea;list-style:none;cursor:default;font-size:1.2em}#contact{padding:50px 15px;background:#3498db;text-align:center}#contact h2{margin:0 0 15px 0;color:#fff;font-weight:500}#contact-form{max-width:500px;margin:0 auto}#contact-form input,#contact-form textarea{display:block;width:100%;padding:10px;border-radius:4px;border:none;margin-bottom:10px;background:#1d6fa5;color:#fff;transition:.5s ease all}#contact-form input::-webkit-input-placeholder,#contact-form textarea::-webkit-input-placeholder{color:#fff}#contact-form input:-moz-placeholder,#contact-form textarea:-moz-placeholder{color:#fff;opacity:1}#contact-form input::-moz-placeholder,#contact-form textarea::-moz-placeholder{color:#fff;opacity:1}#contact-form input:-ms-input-placeholder,#contact-form textarea:-ms-input-placeholder{color:#fff}#contact-form input:focus,#contact-form textarea:focus{outline:none;background:#16527a}#contact-form textarea{height:150px;resize:none}#contact-form button{display:block;width:100%;background:#fff;border-radius:4px;padding:5px 10px;border:none;color:#3498db;font-weight:700;box-shadow:0 1px 3px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24);transition:.5s ease all}#contact-form button:hover{box-shadow:0 10px 20px rgba(0,0,0,0.19),0 6px 6px rgba(0,0,0,0.23)}.optional-section{padding:50px 15px;text-align:center;border-top:1px solid #dcd9d9}.optional-section h2{color:#374054}.optional-section-block{max-width:700px;margin:0 auto 30px auto;padding:15px;border:1px solid #dcd9d9;background:#fff;text-align:left}.optional-section-block h3{font-weight:500;margin:0 0 15px 0;color:#374054}.optional-section-block h4{color:#74808a;clear:both;font-weight:500;margin:0 0 15px 0}.optional-section-block p,.optional-section-block ul{margin:0 0 15px 0;color:#74808a;font-size:0.9em}.optional-section-block ul{padding:0 0 0 15px}@media only screen and (max-width: 750px){#experience-timeline:before,#experience-timeline:after{left:23px}.vtimeline-date{width:auto;text-align:left;position:relative;margin-bottom:15px;display:block;margin-left:70px}.vtimeline-icon{margin-left:0}.vtimeline-content{margin-left:70px}}@media only screen and (max-width: 992px){#lead{height:auto;min-height:auto;max-height:auto;padding:100px 15px}#lead-content{position:relative;transform:none;left:auto;top:auto}#lead-content h1{font-size:3em}#lead-content h2{font-size:1.75em}#about{text-align:center}#about p{text-align:left}}@media only screen and (max-width: 768px){header{position:fixed;display:none;z-index:999;animation:none;bottom:0;height:100%}#mobile-menu-open,#mobile-menu-close{display:block}#menu{height:100%;overflow-y:auto;box-shadow:none;border-radius:0;width:100%}#menu li{display:block;margin-bottom:10px}#lead-content h1{font-size:2em}#lead-content h2{font-size:1.3em}#lead-content a{padding:10px 20px}#lead-down{display:none}.education-block h3,.education-block span{float:none}.project-image{display:none}.project-info{position:relative;margin:0;padding:30px 15px;top:auto;transform:none}footer{text-align:center}.social{text-align:center}}@media only screen and (max-width: 480px){#lead-content h1{font-size:1.5em}#lead-content h2{font-size:1em}#lead-content a{font-size:0.9em;padding:5px 10px}} 13 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2017 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=78a470ef07940263f3fc8d3809589bec) 9 | * Config saved to config.json and https://gist.github.com/78a470ef07940263f3fc8d3809589bec 10 | *//*! 11 | * Bootstrap v3.3.7 (http://getbootstrap.com) 12 | * Copyright 2011-2016 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed} -------------------------------------------------------------------------------- /linkedrw/linkedw/website.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | import os 3 | import pkg_resources 4 | import re 5 | 6 | from collections import defaultdict 7 | from datetime import datetime 8 | from logbook import Logger 9 | 10 | from linkedrw.constants import * 11 | from linkedrw.utils import make_dir, copy_files 12 | 13 | SECTION_ENDS = [ 14 | "End #about", 15 | "End #experience", 16 | "End #education", 17 | "End #projects", 18 | "End #skills", 19 | "End #social", 20 | ] 21 | 22 | 23 | def make_website_files(profile, output_dir): 24 | """ 25 | Create website files 26 | Args: 27 | profile: the dict of the profile 28 | output_dir: the output directory 29 | 30 | Returns: 31 | None 32 | """ 33 | log = Logger() 34 | log.notice("Creating website files...") 35 | 36 | output_dir = os.path.join(output_dir, "website") 37 | make_dir(output_dir) 38 | copy_files(__name__.split(".")[0], "templates/dev_portfolio_files", output_dir) 39 | 40 | lines = [] 41 | comment_line = has_sum = has_exp = has_edu = has_prj = has_skl = has_con = False 42 | 43 | with open( 44 | pkg_resources.resource_filename(__name__.split(".")[0], PORTFOLIO_TEMPLATE) 45 | ) as f: 46 | for line in f: 47 | line = line.strip("\n") 48 | indent = re.match(r"\s+", line) 49 | 50 | if indent is not None: 51 | indent = indent.group() 52 | 53 | if "section-headers-here" in line: 54 | for section in PORTFOLIO_SECTIONS: 55 | if section == CONTACT or (section in profile and profile[section]): 56 | lines += make_section_header(section, indent) 57 | elif NAME in profile and "name-here" in line: 58 | lines.append(line.replace("name-here", profile[NAME])) 59 | elif POSITION in profile and "title-here" in line: 60 | lines.append(line.replace("title-here", profile[POSITION])) 61 | elif NAME in profile and "copyright-here" in line: 62 | lines.append( 63 | line.replace( 64 | "copyright-here", f"{arrow.now().year} {profile[NAME]}" 65 | ) 66 | ) 67 | 68 | # About section 69 | elif 'id="about"' in line: 70 | if SUMMARY not in profile or not profile[SUMMARY]: 71 | comment_line = True 72 | lines += make_comment_line(line) 73 | else: 74 | has_sum = True 75 | lines.append(line) 76 | elif has_sum and "summary-here" in line: 77 | lines += make_summary_section(profile[SUMMARY], indent) 78 | 79 | # Experience section 80 | elif 'id="experience"' in line: 81 | if EXPERIENCE not in profile or not profile[EXPERIENCE]: 82 | comment_line = True 83 | lines += make_comment_line(line) 84 | else: 85 | has_exp = True 86 | lines.append(line) 87 | elif has_exp and "experience-here" in line: 88 | lines += make_experience_section(profile[EXPERIENCE], indent) 89 | 90 | # Education section 91 | elif 'id="education"' in line: 92 | if EDUCATION not in profile or not profile[EDUCATION]: 93 | comment_line = True 94 | lines += make_comment_line(line) 95 | else: 96 | has_edu = True 97 | lines.append(line) 98 | elif has_edu and "education-here" in line: 99 | lines += make_education_section(profile[EDUCATION], indent) 100 | 101 | # Projects section 102 | elif 'id="projects"' in line: 103 | if PROJECTS not in profile or not profile[PROJECTS]: 104 | comment_line = True 105 | lines += make_comment_line(line) 106 | else: 107 | has_prj = True 108 | lines.append(line) 109 | elif has_prj and "projects-here" in line: 110 | lines += make_projects_section(profile[PROJECTS], indent) 111 | 112 | # Skills section 113 | elif 'id="skills"' in line: 114 | if SKILLS not in profile or not profile[SKILLS]: 115 | comment_line = True 116 | lines += make_comment_line(line) 117 | else: 118 | has_skl = True 119 | lines.append(line) 120 | elif has_skl and "skills-here" in line: 121 | lines += make_skills_section(profile[SKILLS], indent) 122 | 123 | # Contact section 124 | elif ( 125 | CONTACT in profile 126 | and EMAIL in profile[CONTACT] 127 | and "email-here" in line 128 | ): 129 | lines.append(line.replace("email-here", profile[CONTACT][EMAIL])) 130 | elif "col-sm-5 social" in line: 131 | if CONTACT not in profile or all( 132 | x not in profile[CONTACT] or not profile[CONTACT][x] 133 | for x in CONTACTS 134 | ): 135 | comment_line = True 136 | lines += make_comment_line(line) 137 | else: 138 | has_con = True 139 | lines.append(line) 140 | elif has_con and "contact-here" in line: 141 | lines += make_contact_section(profile[CONTACT], indent) 142 | 143 | # Comment out sections 144 | elif comment_line and any(x in line for x in SECTION_ENDS): 145 | comment_line = False 146 | elif comment_line: 147 | lines += make_comment_line(line) 148 | 149 | # No changes to line 150 | else: 151 | lines.append(line) 152 | 153 | with open(os.path.join(output_dir, "index.html"), "w") as f: 154 | f.write("\n".join(lines)) 155 | 156 | 157 | def make_section_header(section, indent): 158 | """ 159 | Create the lines for the section header 160 | Args: 161 | section: the section 162 | indent: the original indentation 163 | 164 | Returns: 165 | A list of lines for the section header 166 | """ 167 | lines = [ 168 | f"{indent}
  • ", 169 | f'{indent}{HTML_INDENT}{section.title()}', 170 | f"{indent}
  • ", 171 | ] 172 | 173 | return lines 174 | 175 | 176 | def make_summary_section(summary, indent): 177 | """ 178 | Create the summary line 179 | Args: 180 | summary: the summary text 181 | indent: the original indentation 182 | 183 | Returns: 184 | A string of the summary line 185 | """ 186 | text = summary.replace("\n", "
    ") 187 | line = [f"{indent}{text}"] 188 | 189 | return line 190 | 191 | 192 | def make_experience_section(exps, indent): 193 | """ 194 | Create the lines of the experience section 195 | Args: 196 | exps: the list of experiences 197 | indent: the original indentation 198 | 199 | Returns: 200 | A list of lines of the experience section 201 | """ 202 | sorted_exps = sort_entries(exps) 203 | lines = [] 204 | 205 | for exp in sorted_exps: 206 | lines += [ 207 | f'{indent}
    ', 208 | f"{indent}{HTML_INDENT}

    {exp[NAME]}

    ", 209 | f"{indent}{HTML_INDENT}

    {exp[TITLE]}

    ", 210 | ] 211 | lines += get_description(exp[DESCRIPTION], indent) 212 | lines += [f"{indent}
    ", ""] 213 | 214 | return lines 215 | 216 | 217 | def make_education_section(edus, indent): 218 | """ 219 | Create the lines of the education section 220 | Args: 221 | edus: the list of educations 222 | indent: the original indentation 223 | 224 | Returns: 225 | A list of lines of the education section 226 | """ 227 | sorted_edus = sort_entries(edus, date_format="YYYY") 228 | lines = [] 229 | 230 | for edu in sorted_edus: 231 | lines += [ 232 | f'{indent}
    ', 233 | f"{indent}{HTML_INDENT}

    {edu[NAME]}

    ", 234 | f'{indent}{HTML_INDENT}{edu[DATES]}', 235 | f"{indent}{HTML_INDENT}

    {edu[DEGREE]}

    ", 236 | ] 237 | lines += get_description(edu[DESCRIPTION], indent) 238 | lines += [f"{indent}
    ", ""] 239 | 240 | return lines 241 | 242 | 243 | def make_projects_section(prjs, indent): 244 | """ 245 | Create the lines of the project section 246 | Args: 247 | prjs: the list of projects 248 | indent: the original indentation 249 | 250 | Returns: 251 | A list of lines of the project section 252 | """ 253 | lines = [] 254 | for prj in prjs: 255 | lines += [ 256 | f'{indent}
    ', 257 | f'{indent}{HTML_INDENT}
    ', 258 | f'{indent}{HTML_INDENT * 2}
    ', 259 | f"{indent}{HTML_INDENT * 3}

    {prj[NAME]}

    ", 260 | ] 261 | 262 | lines += get_description(prj[DESCRIPTION], indent + HTML_INDENT * 2) 263 | if prj[LINK]: 264 | lines.append( 265 | f'{indent}{HTML_INDENT * 3}View Project' 266 | ) 267 | 268 | lines += [f"{indent}{HTML_INDENT * x}
    " for x in range(2, -1, -1)] + [""] 269 | 270 | return lines 271 | 272 | 273 | def make_skills_section(skls, indent): 274 | """ 275 | Create the liens of the skill section 276 | Args: 277 | skls: the list of skills 278 | indent: the original indentation 279 | 280 | Returns: 281 | A list of lines of the skill section 282 | """ 283 | lines = [] 284 | for skl in skls: 285 | lines.append(f"{indent}
  • {skl}
  • ") 286 | 287 | return lines 288 | 289 | 290 | def make_contact_section(cons, indent): 291 | """ 292 | Create the lines of the contact section 293 | Args: 294 | cons: the list of contacts 295 | indent: the original indentation 296 | 297 | Returns: 298 | A list of lines of the contact section 299 | """ 300 | lines = [] 301 | for con in CONTACTS: 302 | if con in cons and cons[con]: 303 | lines += [ 304 | f"{indent}
  • ", 305 | f"{indent}{HTML_INDENT}" 306 | f'', 307 | f"{indent}
  • ", 308 | ] 309 | 310 | return lines 311 | 312 | 313 | def sort_entries(entries, date_format="MMM YYYY"): 314 | """ 315 | Sort entries by date 316 | Args: 317 | entries: the list of entries to be sorted 318 | date_format: the format of the date in the entries 319 | 320 | Returns: 321 | A list of sorted entries 322 | """ 323 | all_entries = defaultdict(list) 324 | for exp in entries: 325 | name = exp[NAME] 326 | for entry in exp[ENTRIES]: 327 | entry[NAME] = name 328 | date = entry[DATES].split(" - ")[-1] 329 | 330 | if date.lower() == "present": 331 | arrow_date = arrow.utcnow().date() 332 | elif date: 333 | arrow_date = arrow.get(date, date_format).date() 334 | else: 335 | arrow_date = arrow.get(datetime.min).date() 336 | 337 | all_entries[arrow_date].append(entry) 338 | 339 | sorted_entries = [] 340 | for key in sorted(all_entries, reverse=True): 341 | sorted_entries += all_entries[key] 342 | 343 | return sorted_entries 344 | 345 | 346 | def get_description(descs, indent): 347 | """ 348 | Create the lines of the description 349 | Args: 350 | descs: the list of descriptions 351 | indent: the original indentation 352 | 353 | Returns: 354 | A list of lines of the description 355 | """ 356 | lines = [] 357 | if descs: 358 | lines.append(f"{indent}{HTML_INDENT}
      ") 359 | for desc in descs.split("\n"): 360 | desc = desc.strip("-").strip() 361 | if desc: 362 | lines.append(f"{indent}{HTML_INDENT * 2}
    • {desc}") 363 | 364 | lines.append(f"{indent}{HTML_INDENT}
    ") 365 | 366 | return lines 367 | 368 | 369 | def make_comment_line(line): 370 | return [f""] 371 | -------------------------------------------------------------------------------- /linkedrw/scraper/background.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from collections import defaultdict 4 | from selenium.common.exceptions import NoSuchElementException 5 | from selenium.webdriver.common.by import By 6 | 7 | from linkedrw.constants import * 8 | from linkedrw.utils import ( 9 | get_span_text, 10 | get_optional_text, 11 | get_description, 12 | scroll_to_elem, 13 | ) 14 | 15 | 16 | def get_background_details(driver, by, section_id, section_type): 17 | """ 18 | Scrape background details 19 | Args: 20 | driver: the selenium details 21 | by: the strategy to locate an element 22 | section_id: the section identifier 23 | section_type: the section type 24 | 25 | Returns: 26 | A list of details of all the items under the given section 27 | """ 28 | # Load background section 29 | if section_type == EXPERIENCE: 30 | scroll_to_elem(driver, By.ID, "oc-background-section") 31 | 32 | # Load the rest of the page 33 | elif section_type == SKILLS: 34 | try: 35 | scroll_to_elem( 36 | driver, 37 | By.CSS_SELECTOR, 38 | ".pv-deferred-area.pv-deferred-area--pending.ember-view", 39 | ) 40 | except NoSuchElementException: 41 | pass 42 | 43 | # Check if the section exists 44 | try: 45 | section = driver.find_element(by, section_id) 46 | except NoSuchElementException: 47 | return [] 48 | 49 | if section_type == EXPERIENCE: 50 | return get_experience(section) 51 | elif section_type == EDUCATION: 52 | return get_education(section) 53 | elif section_type == VOLUNTEERING: 54 | return get_volunteering(section) 55 | elif section_type == SKILLS: 56 | return get_skills(driver, section) 57 | 58 | 59 | def get_experience(section): 60 | """ 61 | Scrape experience details 62 | Args: 63 | section: the experience section 64 | 65 | Returns: 66 | A list of details of all experiences 67 | """ 68 | ul = get_section(section) 69 | entries = ul.find_elements_by_css_selector( 70 | ".pv-profile-section__sortable-item.pv-profile-section__section-info-item.relative." 71 | "pv-profile-section__list-item.sortable-item.ember-view" 72 | ) 73 | entries += ul.find_elements_by_css_selector( 74 | ".pv-entity__position-group-pager.pv-profile-section__list-item.ember-view" 75 | ) 76 | exps = [] 77 | 78 | for entry in entries: 79 | # Check if it is a single role in a company or multiple roles in a company 80 | try: 81 | summary = entry.find_element_by_css_selector( 82 | ".pv-entity__summary-info.pv-entity__summary-info--background-section" 83 | ) 84 | exps.append(get_single_role(entry, summary)) 85 | except NoSuchElementException: 86 | summary = entry.find_element_by_css_selector( 87 | ".pv-profile-section__card-item-v2.pv-profile-section.pv-position-entity.ember-view" 88 | ) 89 | exps.append(get_multiple_roles(entry, summary)) 90 | 91 | return exps 92 | 93 | 94 | def get_single_role(div, summary): 95 | """ 96 | Scrape details of a single role 97 | Args: 98 | div: the div element 99 | summary: the summary section 100 | 101 | Returns: 102 | A dict of the details for a single role 103 | """ 104 | title = summary.find_element_by_css_selector(".t-16.t-black.t-bold").text 105 | company = summary.find_element_by_class_name("pv-entity__secondary-title").text 106 | dates = get_span_text( 107 | summary, ".pv-entity__date-range.t-14.t-black--light.t-normal" 108 | ) 109 | location = get_optional_text( 110 | summary, ".pv-entity__location.t-14.t-black--light.t-normal.block" 111 | ) 112 | description = get_description( 113 | div, ".pv-entity__description.t-14.t-black.t-normal.ember-view" 114 | ) 115 | 116 | results = { 117 | NAME: company, 118 | ENTRIES: [ 119 | {TITLE: title, DATES: dates, LOCATION: location, DESCRIPTION: description} 120 | ], 121 | } 122 | 123 | return results 124 | 125 | 126 | def get_multiple_roles(div, summary): 127 | """ 128 | Scrape details of multiple roles 129 | Args: 130 | div: the div element 131 | summary: the summary section 132 | 133 | Returns: 134 | A dict of the details for multiple roles 135 | """ 136 | try: 137 | role_sections = div.find_elements_by_class_name( 138 | "pv-entity__position-group-role-item" 139 | ) 140 | except NoSuchElementException: 141 | role_sections = div.find_elements_by_class_name( 142 | "pv-entity__position-group-role-item-fading-timeline" 143 | ) 144 | 145 | roles = [] 146 | for role_section in role_sections: 147 | title = get_span_text(role_section, ".t-14.t-black.t-bold") 148 | dates = get_span_text( 149 | role_section, ".pv-entity__date-range.t-14.t-black.t-normal" 150 | ) 151 | location = get_optional_text( 152 | role_section, ".pv-entity__location.t-14.t-black--light.t-normal.block" 153 | ) 154 | description = get_description( 155 | role_section, ".pv-entity__description.t-14.t-black.t-normal.ember-view" 156 | ) 157 | 158 | roles.append( 159 | {TITLE: title, DATES: dates, LOCATION: location, DESCRIPTION: description} 160 | ) 161 | 162 | results = {NAME: get_span_text(summary, ".t-16.t-black.t-bold"), ENTRIES: roles} 163 | 164 | return results 165 | 166 | 167 | def get_education(section): 168 | """ 169 | Scrape education details 170 | Args: 171 | section: the education section 172 | 173 | Returns: 174 | A list of details of all educations 175 | """ 176 | ul = get_section(section) 177 | edu_dict = defaultdict(list) 178 | 179 | for li in ul.find_elements_by_tag_name("li"): 180 | school = li.find_element_by_css_selector( 181 | ".pv-entity__school-name.t-16.t-black.t-bold" 182 | ).text 183 | degree_name = get_span_text( 184 | li, 185 | ".pv-entity__secondary-title.pv-entity__degree-name.pv-entity__secondary-title.t-14.t-black.t-normal", 186 | ) 187 | dates = get_optional_text(li, ".pv-entity__dates.t-14.t-black--light.t-normal") 188 | description = get_description( 189 | li, ".pv-entity__description.t-14.t-black--light.t-normal.mt4" 190 | ) 191 | 192 | # Check if there is a degree name, if not, skip this entry 193 | if not degree_name: 194 | continue 195 | 196 | edu_dict[school].append( 197 | { 198 | DEGREE: get_degree(li, degree_name), 199 | LOCATION: "", 200 | DATES: dates, 201 | DESCRIPTION: description, 202 | } 203 | ) 204 | 205 | edu_list = [] 206 | for school in edu_dict: 207 | edu_list.append({NAME: school, ENTRIES: edu_dict[school]}) 208 | 209 | return edu_list 210 | 211 | 212 | def get_degree(li, degree_name): 213 | """ 214 | Get the full degree description 215 | Args: 216 | li: the li element 217 | degree_name: the degree name 218 | 219 | Returns: 220 | A string of the degree description 221 | """ 222 | 223 | # Check if there is a field of study 224 | try: 225 | field = get_span_text( 226 | li, 227 | ".pv-entity__secondary-title.pv-entity__fos.pv-entity__secondary-title.t-14.t-black--light.t-normal", 228 | ) 229 | degree = f"{degree_name} - {field}" 230 | except NoSuchElementException: 231 | degree = degree_name 232 | 233 | return degree 234 | 235 | 236 | def get_volunteering(section): 237 | """ 238 | Scrape volunteering details 239 | Args: 240 | section: the volunteering section 241 | 242 | Returns: 243 | A list of details of all volunteering 244 | """ 245 | ul = get_section(section) 246 | vol_dict = defaultdict(list) 247 | 248 | for li in ul.find_elements_by_tag_name("li"): 249 | title = li.find_element_by_css_selector(".t-16.t-black.t-bold").text 250 | organisation = get_span_text(li, ".t-14.t-black.t-normal") 251 | dates = get_optional_text( 252 | li, 253 | ".pv-entity__date-range.detail-facet.inline-block.t-14.t-black--light.t-normal", 254 | ) 255 | description = get_description( 256 | li, ".pv-entity__description.t-14.t-black--light.t-normal.mt4" 257 | ) 258 | 259 | vol_dict[organisation].append( 260 | {TITLE: title, DATES: dates, LOCATION: "", DESCRIPTION: description} 261 | ) 262 | 263 | vol_list = [] 264 | for organisation in vol_dict: 265 | vol_list.append({NAME: organisation, ENTRIES: vol_dict[organisation]}) 266 | 267 | return vol_list 268 | 269 | 270 | def get_skills(driver, section): 271 | """ 272 | Scrape skills 273 | Args: 274 | driver: the web driver 275 | section: the skills section 276 | 277 | Returns: 278 | A list of skills 279 | """ 280 | 281 | # Show all skills 282 | try: 283 | btn = scroll_to_elem( 284 | driver, By.CLASS_NAME, "pv-skills-section__chevron-icon", align="false" 285 | ) 286 | btn.click() 287 | except NoSuchElementException: 288 | pass 289 | 290 | # Extract top skills 291 | skills = [] 292 | for top_skill in section.find_elements_by_css_selector( 293 | ".pv-skill-category-entity__top-skill.pv-skill-category-entity.pb3.pt4.pv-skill-endorsedSkill-entity." 294 | "relative.ember-view" 295 | ): 296 | skill = top_skill.find_element_by_css_selector( 297 | ".pv-skill-category-entity__name-text.t-16.t-black.t-bold" 298 | ).text 299 | skills.append(skill.split("\n")[0]) 300 | 301 | # Locate Tools & Technologies section 302 | target_div = None 303 | for div in section.find_elements_by_css_selector( 304 | ".pv-skill-category-list.pv-profile-section__section-info.mb6.ember-view" 305 | ): 306 | header = div.find_element_by_tag_name("h3") 307 | if header.text.lower() == "tools & technologies": 308 | target_div = div 309 | break 310 | 311 | # Extract the rest of the skills 312 | if target_div is not None: 313 | for li in target_div.find_elements_by_tag_name("li"): 314 | skills.append(li.text.split("\n")[0]) 315 | 316 | return skills 317 | 318 | 319 | def get_section(section): 320 | """ 321 | Get the items section 322 | Args: 323 | section: the section 324 | 325 | Returns: 326 | The items section 327 | """ 328 | is_expanded = expand_section(section) 329 | if is_expanded: 330 | # The ul element can appear in two different classes 331 | try: 332 | elem = section.find_element_by_css_selector( 333 | ".pv-profile-section__section-info.section-info.pv-profile-section__section-info--has-more.ember-view" 334 | ) 335 | except NoSuchElementException: 336 | elem = section.find_element_by_css_selector( 337 | ".pv-profile-section__section-info.section-info.pv-profile-section__section-info--has-more" 338 | ) 339 | else: 340 | # The ul element can appear in two different classes 341 | try: 342 | elem = section.find_element_by_css_selector( 343 | ".pv-profile-section__section-info.section-info.pv-profile-section__section-info--has-no-more." 344 | "ember-view" 345 | ) 346 | except NoSuchElementException: 347 | elem = section.find_element_by_css_selector( 348 | ".pv-profile-section__section-info.section-info.pv-profile-section__section-info--has-no-more" 349 | ) 350 | 351 | return elem 352 | 353 | 354 | def expand_section(section): 355 | css = ".pv-profile-section__see-more-inline.pv-profile-section__text-truncate-toggle.link" 356 | btns = [] 357 | count = 0 358 | 359 | all_btns = section.find_elements_by_css_selector(css) 360 | if all_btns: 361 | btns.append(all_btns[-1]) 362 | 363 | while btns: 364 | btn = btns.pop() 365 | text = btn.text.lower() 366 | btn.click() 367 | time.sleep(1) 368 | 369 | if EXPERIENCE in text or EDUCATION in text: 370 | count += 1 371 | 372 | new_btns = section.find_elements_by_css_selector(css) 373 | if new_btns: 374 | btns.append(new_btns[-1]) 375 | 376 | return count > 0 377 | -------------------------------------------------------------------------------- /linkedrw/templates/dev_portfolio_files/scss/styles.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | Title: Dev Portfolio Template 3 | Version: 1.2.1 4 | Last Change: 08/28/2017 5 | Author: Ryan Fitzgerald 6 | Repo: https://github.com/RyanFitzgerald/devportfolio-template 7 | Issues: https://github.com/RyanFitzgerald/devportfolio-template/issues 8 | 9 | Description: This file contains all the styles associated with the page 10 | that don't come from third party libraries. This file gets compiled using 11 | Gulp and send to the /css folder which is then loaded on the page. 12 | */ 13 | /* 14 | File Contents: 15 | 16 | 1. Variables 17 | 2. Mixins 18 | 3. Global Styles 19 | 4. Lead Styles 20 | 5. About Styles 21 | 6. Experience Styles 22 | 7. Education Styles 23 | 8. Project Styles 24 | 9. Skills Styles 25 | 10. Contact Styles 26 | 11. Optional Section Styles 27 | 12. Media Queries 28 | */ 29 | 30 | /* 31 | ------------------------ 32 | ----- 1. Variables ----- 33 | ------------------------ 34 | */ 35 | 36 | // Define base and accent colors 37 | $base-color: #3498db; 38 | $base-color-hover: darken($base-color, 10%); 39 | 40 | // Define background colors 41 | $background: #fff; 42 | $background-alt: #f2f2f5; 43 | 44 | // Define border colors 45 | $border: #dcd9d9; 46 | 47 | // Define text colors 48 | $heading: #374054; 49 | $text: #74808a; 50 | 51 | /* 52 | --------------------- 53 | ----- 2. Mixins ----- 54 | --------------------- 55 | */ 56 | 57 | @mixin transition($duration: 0.5s, $func: ease, $property: all) { 58 | transition: #{$duration} #{$func} #{$property}; 59 | } 60 | 61 | @mixin placeholder { 62 | &::-webkit-input-placeholder { 63 | @content; 64 | } 65 | 66 | &:-moz-placeholder { 67 | @content; 68 | opacity: 1; 69 | } 70 | 71 | &::-moz-placeholder { 72 | @content; 73 | opacity: 1; 74 | } 75 | 76 | &:-ms-input-placeholder { 77 | @content; 78 | } 79 | } 80 | 81 | /* 82 | ---------------------------- 83 | ----- 3. Global Styles ----- 84 | ---------------------------- 85 | */ 86 | 87 | body { 88 | font-family: 'Lato', sans-serif; 89 | font-size: 16px; 90 | 91 | &.active { 92 | overflow: hidden; 93 | z-index: -1; 94 | } 95 | } 96 | 97 | // No JS styles 98 | .no-js { 99 | #experience-timeline { 100 | > div { 101 | background: $background; 102 | padding: 10px; 103 | margin-bottom: 10px; 104 | border: 1px solid $border; 105 | 106 | h3 { 107 | font-size: 1.5em; 108 | font-weight: 300; 109 | color: $heading; 110 | display: inline-block; 111 | margin: 0; 112 | } 113 | 114 | h4 { 115 | font-size: 1.2em; 116 | font-weight: 300; 117 | color: #7e8890; 118 | margin: 0 0 15px 0; 119 | } 120 | 121 | p { 122 | color: $text; 123 | font-size: 0.9em; 124 | margin: 0; 125 | } 126 | } 127 | 128 | &:before, &:after { 129 | content: none; 130 | } 131 | } 132 | } 133 | 134 | @keyframes dropHeader { 135 | 0% { 136 | transform: translateY(-100%); 137 | } 138 | 100% { 139 | transform: translateY(0); 140 | } 141 | } 142 | 143 | header { 144 | position: absolute; 145 | top: 0; 146 | left: 0; 147 | right: 0; 148 | text-align: center; 149 | z-index: 10; 150 | animation-name: dropHeader; 151 | animation-iteration-count: 1; 152 | animation-timing-function: ease; 153 | animation-duration: 0.75s; 154 | 155 | ul { 156 | display: inline-block; 157 | background: $background; 158 | text-align: center; 159 | padding: 10px; 160 | margin: 0; 161 | border-bottom-right-radius: 4px; 162 | border-bottom-left-radius: 4px; 163 | } 164 | 165 | li { 166 | display: inline-block; 167 | } 168 | 169 | a { 170 | display: block; 171 | color: $base-color; 172 | padding: 10px; 173 | 174 | &:hover { 175 | color: $base-color-hover; 176 | text-decoration: none; 177 | background: #eee; 178 | border-radius: 4px; 179 | } 180 | 181 | &:focus { 182 | color: $base-color; 183 | text-decoration: none; 184 | } 185 | } 186 | 187 | &.active { 188 | display: block; 189 | } 190 | 191 | &.sticky { 192 | position: fixed; 193 | z-index: 999; 194 | } 195 | } 196 | 197 | #menu { 198 | &.active { 199 | display: block; 200 | } 201 | } 202 | 203 | #mobile-menu-open { 204 | display: none; 205 | cursor: pointer; 206 | position: fixed; 207 | right: 15px; 208 | top: 10px; 209 | color: $base-color; 210 | font-size: 1.5em; 211 | z-index: 20; 212 | padding: 0 7px; 213 | border-radius: 4px; 214 | background: $background; 215 | } 216 | 217 | #mobile-menu-close { 218 | display: none; 219 | text-align: right; 220 | width: 100%; 221 | background: $background; 222 | font-size: 1.5em; 223 | padding-right: 15px; 224 | padding-top: 10px; 225 | cursor: pointer; 226 | color: $base-color; 227 | 228 | span { 229 | font-size: 0.5em; 230 | text-transform: uppercase; 231 | } 232 | 233 | i { 234 | vertical-align: middle; 235 | } 236 | } 237 | 238 | footer { 239 | padding: 50px 0; 240 | } 241 | 242 | .copyright { 243 | padding-top: 20px; 244 | 245 | p { 246 | margin: 0; 247 | color: $text; 248 | } 249 | } 250 | 251 | .top { 252 | text-align: center; 253 | 254 | span { 255 | cursor: pointer; 256 | display: block; 257 | margin: 15px auto 0 auto; 258 | width: 35px; 259 | height: 35px; 260 | border-radius: 50%; 261 | border: 3px solid lighten($text, 25%); 262 | text-align:center; 263 | } 264 | 265 | i { 266 | color: $text; 267 | } 268 | } 269 | 270 | .social { 271 | text-align: right; 272 | 273 | ul { 274 | margin: 5px 0 0 0; 275 | padding: 0; 276 | } 277 | 278 | li { 279 | display: inline-block; 280 | font-size: 1.25em; 281 | list-style: none; 282 | } 283 | 284 | a { 285 | display: block; 286 | color: $text; 287 | padding: 10px; 288 | 289 | &:hover { 290 | color: $base-color; 291 | } 292 | } 293 | } 294 | 295 | .btn-rounded-white { 296 | display: inline-block; 297 | color: #fff; 298 | padding: 15px 25px; 299 | border: 3px solid #fff; 300 | border-radius: 30px; 301 | @include transition(); 302 | 303 | &:hover { 304 | color: $base-color; 305 | background: #fff; 306 | text-decoration: none; 307 | } 308 | } 309 | 310 | .shadow { 311 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 312 | } 313 | 314 | .shadow-large { 315 | box-shadow: 0 3px 6px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.15); 316 | } 317 | 318 | .heading { 319 | position: relative; 320 | display: inline-block; 321 | font-size: 2em; 322 | font-weight: 300; 323 | margin: 0 0 30px 0; 324 | 325 | &:after { 326 | position: absolute; 327 | content: ''; 328 | top: 100%; 329 | height: 1px; 330 | width: 50px; 331 | left: 0; 332 | right: 0; 333 | margin: 0 auto; 334 | background: $base-color; 335 | } 336 | } 337 | 338 | .background-alt { 339 | background: $background-alt; 340 | } 341 | 342 | /* 343 | -------------------------- 344 | ----- 4. Lead Styles ----- 345 | -------------------------- 346 | */ 347 | 348 | #lead { 349 | position: relative; 350 | height: 100vh; 351 | min-height: 500px; 352 | max-height: 1080px; 353 | background: url(../images/lead-bg.jpg); 354 | background-size: cover; 355 | padding: 15px; 356 | overflow: hidden; 357 | } 358 | 359 | #lead-content { 360 | position: absolute; 361 | z-index: 10; 362 | top: 50%; 363 | left: 50%; 364 | transform: translate(-50%, -50%); 365 | text-align: center; 366 | 367 | h1, h2 { 368 | margin: 0; 369 | } 370 | 371 | h1 { 372 | color: #fff; 373 | font-weight: 900; 374 | font-size: 5em; 375 | text-transform: uppercase; 376 | letter-spacing: 0.05em; 377 | line-height: 0.9em; 378 | } 379 | 380 | h2 { 381 | color: lighten($base-color, 25%); 382 | font-weight: 500; 383 | font-size: 2.25em; 384 | margin-bottom: 15px; 385 | } 386 | } 387 | 388 | #lead-overlay { 389 | position: absolute; 390 | height: 100%; 391 | width: 100%; 392 | top: 0; 393 | right: 0; 394 | bottom: 0; 395 | left: 0; 396 | background: rgba($base-color-hover, 0.8); 397 | z-index: 1; 398 | } 399 | 400 | #lead-down { 401 | position: absolute; 402 | left: 0; 403 | right: 0; 404 | width: 100%; 405 | text-align: center; 406 | z-index: 10; 407 | bottom: 15px; 408 | color: #fff; 409 | 410 | span { 411 | cursor: pointer; 412 | display: block; 413 | margin: 0 auto; 414 | width: 35px; 415 | height: 35px; 416 | border-radius: 50%; 417 | border: 3px solid lighten($base-color, 25%); 418 | text-align:center; 419 | } 420 | 421 | i { 422 | animation: pulsate 1.5s ease; 423 | animation-iteration-count: infinite; 424 | padding-top: 5px;; 425 | } 426 | } 427 | 428 | @keyframes pulsate { 429 | 0% { 430 | transform: scale(1, 1); 431 | } 432 | 433 | 50% { 434 | transform: scale(1.2, 1.2); 435 | } 436 | 437 | 100% { 438 | transform: scale(1, 1); 439 | } 440 | } 441 | 442 | /* 443 | --------------------------- 444 | ----- 5. About Styles ----- 445 | --------------------------- 446 | */ 447 | 448 | #about { 449 | padding: 75px 15px; 450 | border-bottom: 1px solid $border; 451 | 452 | h2 { 453 | color: $heading; 454 | } 455 | 456 | p { 457 | color: $text; 458 | margin: 0; 459 | } 460 | } 461 | 462 | /* 463 | -------------------------------- 464 | ----- 6. Experience Styles ----- 465 | -------------------------------- 466 | */ 467 | 468 | #experience { 469 | padding: 50px 15px; 470 | text-align: center; 471 | border-bottom: 1px solid $border; 472 | 473 | h2 { 474 | color: $heading; 475 | } 476 | } 477 | 478 | #experience-timeline { 479 | margin: 30px auto 0 auto; 480 | position: relative; 481 | max-width: 1000px; 482 | 483 | &:before { 484 | position: absolute; 485 | content: ''; 486 | top: 0; 487 | bottom: 0; 488 | left: 303px; 489 | right: auto; 490 | height: 100%; 491 | width: 3px; 492 | background: $base-color; 493 | z-index: 0; 494 | } 495 | 496 | &:after { 497 | position: absolute; 498 | content: ''; 499 | width: 3px; 500 | height: 40px; 501 | background: $base-color; 502 | background: linear-gradient(to bottom, $base-color, rgba($base-color, 0)); 503 | top: 100%; 504 | left: 303px; 505 | } 506 | } 507 | 508 | .vtimeline-content { 509 | margin-left: 350px; 510 | background: #fff; 511 | border: 1px solid #e6e6e6; 512 | padding: 15px; 513 | border-radius: 3px; 514 | text-align: left; 515 | 516 | h3 { 517 | font-size: 1.5em; 518 | font-weight: 300; 519 | color: $heading; 520 | display: inline-block; 521 | margin: 0; 522 | } 523 | 524 | h4 { 525 | font-size: 1.2em; 526 | font-weight: 300; 527 | color: #7e8890; 528 | margin: 0 0 15px 0; 529 | } 530 | 531 | p, ul { 532 | color: $text; 533 | font-size: 0.9em; 534 | margin: 0; 535 | } 536 | 537 | ul { 538 | padding: 0 0 0 15px; 539 | } 540 | } 541 | 542 | .vtimeline-point { 543 | position: relative; 544 | display: block; 545 | vertical-align: top; 546 | margin-bottom: 30px; 547 | } 548 | 549 | .vtimeline-icon { 550 | position: relative; 551 | color: #fff; 552 | width: 50px; 553 | height: 50px; 554 | background: $base-color; 555 | border-radius: 50%; 556 | float: left; 557 | z-index: 99; 558 | margin-left: 280px; 559 | 560 | i { 561 | display: block; 562 | font-size: 2em; 563 | margin-top: 10px; 564 | } 565 | } 566 | 567 | .vtimeline-date { 568 | width: 260px; 569 | text-align: right; 570 | position: absolute; 571 | left: 0; 572 | top: 10px; 573 | font-weight: 300; 574 | color: #374054; 575 | } 576 | 577 | /* 578 | ------------------------------- 579 | ----- 7. Education Styles ----- 580 | ------------------------------- 581 | */ 582 | 583 | #education { 584 | padding: 50px 15px 20px 15px; 585 | border-bottom: 1px solid $border; 586 | text-align: center; 587 | 588 | h2 { 589 | color: $heading; 590 | margin-bottom: 50px; 591 | } 592 | } 593 | 594 | .education-block { 595 | max-width: 700px; 596 | margin: 0 auto 30px auto; 597 | padding: 15px; 598 | border: 1px solid $border; 599 | text-align: left; 600 | 601 | h3 { 602 | font-weight: 500; 603 | float: left; 604 | margin: 0; 605 | color: $heading; 606 | } 607 | 608 | span { 609 | color: $text; 610 | float: right; 611 | } 612 | 613 | h4 { 614 | color: $text; 615 | clear: both; 616 | font-weight: 500; 617 | margin: 0 0 15px 0; 618 | } 619 | 620 | p, ul { 621 | margin: 0; 622 | color: $text; 623 | font-size: 0.9em; 624 | } 625 | 626 | ul { 627 | padding: 0 0 0 15px; 628 | } 629 | } 630 | 631 | /* 632 | ------------------------------- 633 | ----- 8. Project Styles ----- 634 | ------------------------------- 635 | */ 636 | 637 | #projects { 638 | padding: 50px 15px; 639 | border-bottom: 1px solid $border; 640 | text-align: center; 641 | 642 | h2 { 643 | color: $heading; 644 | margin-bottom: 50px; 645 | } 646 | } 647 | 648 | .project { 649 | position: relative; 650 | max-width: 900px; 651 | margin: 0 auto 30px auto; 652 | overflow: hidden; 653 | background: #fff; 654 | border-radius: 4px; 655 | } 656 | 657 | .project-image { 658 | float: left; 659 | } 660 | 661 | .project-info { 662 | position: absolute; 663 | top: 50%; 664 | transform: translateY(-50%); 665 | margin-left: 300px; 666 | padding: 15px; 667 | 668 | h3 { 669 | font-size: 1.5em; 670 | font-weight: 300; 671 | color: $heading; 672 | margin: 0 0 15px 0; 673 | } 674 | 675 | p, ul { 676 | color: $text; 677 | margin: 0 0 15px 0; 678 | font-size: 0.9em; 679 | text-align: left; 680 | } 681 | } 682 | 683 | .no-image { 684 | .project-info { 685 | position: relative; 686 | margin: 0; 687 | padding: 30px 15px; 688 | transform: none; 689 | } 690 | } 691 | 692 | #more-projects { 693 | display: none; 694 | } 695 | 696 | /* 697 | ------------------------------- 698 | ----- 9. Skills Styles ----- 699 | ------------------------------- 700 | */ 701 | 702 | #skills { 703 | padding: 50px 15px; 704 | text-align: center; 705 | 706 | h2 { 707 | color: $heading; 708 | margin-bottom: 50px; 709 | } 710 | 711 | ul { 712 | display: block; 713 | margin: 0 auto; 714 | padding: 0; 715 | max-width: 800px; 716 | } 717 | 718 | li { 719 | display: inline-block; 720 | margin: 7px; 721 | padding: 5px 10px; 722 | color: $heading; 723 | background: darken($background-alt, 5%); 724 | list-style: none; 725 | cursor: default; 726 | font-size: 1.2em; 727 | } 728 | } 729 | 730 | /* 731 | ------------------------------- 732 | ----- 10. Contact Styles ----- 733 | ------------------------------- 734 | */ 735 | 736 | #contact { 737 | padding: 50px 15px; 738 | background: $base-color; 739 | text-align: center; 740 | 741 | h2 { 742 | margin: 0 0 15px 0; 743 | color: #fff; 744 | font-weight: 500; 745 | } 746 | } 747 | 748 | #contact-form { 749 | max-width: 500px; 750 | margin: 0 auto; 751 | 752 | input, textarea { 753 | display: block; 754 | width: 100%; 755 | padding: 10px; 756 | border-radius: 4px; 757 | border: none; 758 | margin-bottom: 10px; 759 | background: darken($base-color, 15%); 760 | color: #fff; 761 | @include transition(); 762 | @include placeholder { 763 | color: #fff; 764 | } 765 | 766 | &:focus { 767 | outline: none; 768 | background: darken($base-color, 25%); 769 | } 770 | } 771 | 772 | textarea { 773 | height: 150px; 774 | resize: none; 775 | } 776 | 777 | button { 778 | display: block; 779 | width: 100%; 780 | background: #fff; 781 | border-radius: 4px; 782 | padding: 5px 10px; 783 | border: none; 784 | color: $base-color; 785 | font-weight: 700; 786 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 787 | @include transition(); 788 | 789 | &:hover { 790 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 791 | } 792 | } 793 | } 794 | 795 | /* 796 | --------------------------------------- 797 | ----- 11. Optional Section Styles ----- 798 | --------------------------------------- 799 | */ 800 | 801 | .optional-section { 802 | padding: 50px 15px; 803 | text-align: center; 804 | border-top: 1px solid $border; 805 | 806 | h2 { 807 | color: $heading; 808 | } 809 | } 810 | 811 | .optional-section-block { 812 | max-width: 700px; 813 | margin: 0 auto 30px auto; 814 | padding: 15px; 815 | border: 1px solid $border; 816 | background: #fff; 817 | text-align: left; 818 | 819 | h3 { 820 | font-weight: 500; 821 | margin: 0 0 15px 0; 822 | color: $heading; 823 | } 824 | 825 | h4 { 826 | color: $text; 827 | clear: both; 828 | font-weight: 500; 829 | margin: 0 0 15px 0; 830 | } 831 | 832 | p, ul { 833 | margin: 0 0 15px 0; 834 | color: $text; 835 | font-size: 0.9em; 836 | } 837 | 838 | ul { 839 | padding: 0 0 0 15px; 840 | } 841 | } 842 | 843 | /* 844 | ----------------------------- 845 | ----- 12. Media Queries ----- 846 | ----------------------------- 847 | */ 848 | 849 | // Collapse timeline 850 | @media only screen and (max-width: 750px) { 851 | 852 | #experience-timeline { 853 | &:before, &:after { 854 | left: 23px; 855 | } 856 | 857 | } 858 | 859 | .vtimeline-date { 860 | width: auto; 861 | text-align: left; 862 | position: relative; 863 | margin-bottom: 15px; 864 | display: block; 865 | margin-left: 70px; 866 | } 867 | 868 | .vtimeline-icon { 869 | margin-left: 0; 870 | } 871 | 872 | .vtimeline-content { 873 | margin-left: 70px; 874 | } 875 | 876 | } 877 | 878 | // Medium Devices 879 | @media only screen and (max-width : 992px) { 880 | 881 | #lead { 882 | height: auto; 883 | min-height: auto; 884 | max-height: auto; 885 | padding: 100px 15px; 886 | } 887 | 888 | #lead-content { 889 | position: relative; 890 | transform: none; 891 | left: auto; 892 | top: auto; 893 | 894 | h1 { 895 | font-size: 3em; 896 | } 897 | 898 | h2 { 899 | font-size: 1.75em; 900 | } 901 | } 902 | 903 | #about { 904 | text-align: center; 905 | 906 | p { 907 | text-align: left; 908 | } 909 | } 910 | 911 | } 912 | 913 | // Small Devices 914 | @media only screen and (max-width : 768px) { 915 | 916 | header { 917 | position: fixed; 918 | display: none; 919 | z-index: 999; 920 | animation: none; 921 | bottom: 0; 922 | height: 100%; 923 | } 924 | 925 | #mobile-menu-open, #mobile-menu-close { 926 | display: block; 927 | } 928 | 929 | #menu { 930 | height: 100%; 931 | overflow-y: auto; 932 | box-shadow: none; 933 | border-radius: 0; 934 | width: 100%; 935 | 936 | li { 937 | display: block; 938 | margin-bottom: 10px; 939 | } 940 | } 941 | 942 | #lead-content { 943 | h1 { 944 | font-size: 2em; 945 | } 946 | 947 | h2 { 948 | font-size: 1.3em; 949 | } 950 | 951 | a { 952 | padding: 10px 20px; 953 | } 954 | } 955 | 956 | #lead-down { 957 | display: none; 958 | } 959 | 960 | .education-block { 961 | h3, span { 962 | float: none; 963 | } 964 | } 965 | 966 | .project-image { 967 | display: none; 968 | } 969 | 970 | .project-info { 971 | position: relative; 972 | margin: 0; 973 | padding: 30px 15px; 974 | top: auto; 975 | transform: none; 976 | } 977 | 978 | footer { 979 | text-align: center; 980 | } 981 | 982 | .social { 983 | text-align: center; 984 | } 985 | 986 | } 987 | 988 | // Extra Small Devices 989 | @media only screen and (max-width : 480px) { 990 | 991 | #lead-content { 992 | h1 { 993 | font-size: 1.5em; 994 | } 995 | 996 | h2 { 997 | font-size: 1em; 998 | } 999 | 1000 | a { 1001 | font-size: 0.9em; 1002 | padding: 5px 10px; 1003 | } 1004 | } 1005 | 1006 | } 1007 | --------------------------------------------------------------------------------